aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml33
-rw-r--r--.ci/azure-pipelines-test.yml2
-rw-r--r--.ci/azure-pipelines.yml6
-rw-r--r--.vscode/launch.json6
-rw-r--r--CONTRIBUTORS.md3
-rw-r--r--Emby.Dlna/DlnaManager.cs48
-rw-r--r--Emby.Dlna/Images/logo240.jpgbin11520 -> 11483 bytes
-rw-r--r--Emby.Dlna/Images/people48.pngbin286 -> 278 bytes
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs92
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs20
-rw-r--r--Emby.Dlna/PlayTo/SsdpHttpClient.cs4
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs16
-rw-r--r--Emby.Drawing/ImageProcessor.cs2
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParser.cs26
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs3
-rw-r--r--Emby.Naming/AudioBook/AudioBookResolver.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs135
-rw-r--r--Emby.Server.Implementations/Browser/BrowserLauncher.cs51
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs2
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs13
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs83
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs5
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj12
-rw-r--r--Emby.Server.Implementations/EntryPoints/StartupWizard.cs83
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs1
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs2
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs5
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs33
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs13
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json34
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json117
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json117
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json54
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs1
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManifest.cs60
-rw-r--r--Emby.Server.Implementations/Security/AuthenticationRepository.cs5
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs4
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs6
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs53
-rw-r--r--Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs18
-rw-r--r--Jellyfin.Api/Attributes/ProducesFileAttribute.cs28
-rw-r--r--Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs18
-rw-r--r--Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs18
-rw-r--r--Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs18
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs3
-rw-r--r--Jellyfin.Api/Controllers/AlbumsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs4
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs9
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs4
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs7
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs3
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs8
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs10
-rw-r--r--Jellyfin.Api/Controllers/DlnaController.cs7
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs34
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs83
-rw-r--r--Jellyfin.Api/Controllers/EnvironmentController.cs6
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs3
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs17
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs24
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs149
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs14
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs20
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs6
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs22
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs35
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs11
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs3
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs26
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs28
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs19
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs10
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs10
-rw-r--r--Jellyfin.Api/Controllers/ScheduledTasksController.cs8
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs77
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs39
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs11
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs8
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs49
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs21
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs16
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs3
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs10
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs9
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs3
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs8
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs4
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs5
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj8
-rw-r--r--Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs16
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj4
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj8
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj4
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs42
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs461
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.cs51
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs6
-rw-r--r--Jellyfin.Server/Configuration/CorsPolicyProvider.cs49
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs24
-rw-r--r--Jellyfin.Server/Filters/FileResponseFilter.cs52
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj19
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs6
-rw-r--r--Jellyfin.Server/Middleware/ExceptionMiddleware.cs1
-rw-r--r--Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs4
-rw-r--r--Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs3
-rw-r--r--Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs4
-rw-r--r--Jellyfin.Server/Models/ServerCorsPolicy.cs30
-rw-r--r--Jellyfin.Server/Program.cs7
-rw-r--r--Jellyfin.Server/Properties/launchSettings.json12
-rw-r--r--Jellyfin.Server/Startup.cs12
-rw-r--r--Jellyfin.Server/StartupOptions.cs4
-rw-r--r--MediaBrowser.Common/Extensions/HttpContextExtensions.cs46
-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/Updates/IInstallationManager.cs4
-rw-r--r--MediaBrowser.Controller/Channels/InternalChannelFeatures.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs1
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs1
-rw-r--r--MediaBrowser.Controller/Entities/IHasMediaSources.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Photo.cs1
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs1
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs1
-rw-r--r--MediaBrowser.Controller/Extensions/StringExtensions.cs6
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs1
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs4
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs1
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs3
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs1
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHost.cs1
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs1
-rw-r--r--MediaBrowser.Controller/LiveTv/ProgramInfo.cs7
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingInfo.cs1
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerInfo.cs1
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs2
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs1
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs44
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs1
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs1
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs5
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs15
-rw-r--r--MediaBrowser.Model/Dlna/DeviceIdentification.cs6
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionNormalizer.cs1
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs14
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs1
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs8
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs2
-rw-r--r--MediaBrowser.Model/Entities/MetadataProvider.cs4
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs1
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/Providers/RemoteImageInfo.cs1
-rw-r--r--MediaBrowser.Model/Providers/RemoteSearchResult.cs1
-rw-r--r--MediaBrowser.Model/Querying/ItemFields.cs1
-rw-r--r--MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs3
-rw-r--r--MediaBrowser.Model/Session/GeneralCommand.cs2
-rw-r--r--MediaBrowser.Model/Session/PlaybackProgressInfo.cs3
-rw-r--r--MediaBrowser.Model/Sync/SyncCategory.cs2
-rw-r--r--MediaBrowser.Model/System/PublicSystemInfo.cs8
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs5
-rw-r--r--MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs1
-rw-r--r--MediaBrowser.Model/Updates/PackageInfo.cs10
-rw-r--r--MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs1
-rw-r--r--MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs (renamed from MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs)0
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs32
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs56
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs80
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs4
-rw-r--r--MediaBrowser.Providers/Manager/ProviderUtils.cs4
-rw-r--r--MediaBrowser.Providers/Manager/RefreshResult.cs15
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj8
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs29
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs72
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs23
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs31
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs18
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs104
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs9
-rw-r--r--MediaBrowser.Providers/Movies/ImdbExternalId.cs (renamed from MediaBrowser.Providers/Movies/MovieExternalIds.cs)18
-rw-r--r--MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Music/AlbumInfoExtensions.cs (renamed from MediaBrowser.Providers/Music/Extensions.cs)0
-rw-r--r--MediaBrowser.Providers/Music/ImvdbId.cs (renamed from MediaBrowser.Providers/Music/MusicExternalIds.cs)0
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs81
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs (renamed from MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs)33
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs (renamed from MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs)11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs34
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs53
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs8
-rw-r--r--MediaBrowser.Providers/Studios/StudiosImageProvider.cs16
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs2
-rw-r--r--MediaBrowser.Providers/TV/DummySeasonProvider.cs15
-rw-r--r--MediaBrowser.Providers/TV/MissingEpisodeProvider.cs46
-rw-r--r--MediaBrowser.Providers/TV/SeasonMetadataService.cs6
-rw-r--r--MediaBrowser.Providers/TV/TvExternalIds.cs82
-rw-r--r--MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs28
-rw-r--r--MediaBrowser.Providers/TV/TvdbExternalId.cs28
-rw-r--r--MediaBrowser.Providers/TV/TvdbSeasonExternalId.cs28
-rw-r--r--MediaBrowser.Providers/TV/Zap2ItExternalId.cs28
-rw-r--r--MediaBrowser.sln1
-rw-r--r--README.md9
-rw-r--r--RSSDP/DisposableManagedObjectBase.cs4
-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
-rw-r--r--fedora/jellyfin.spec4
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj4
-rw-r--r--tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs30
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs90
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs57
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs16
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs1
274 files changed, 3281 insertions, 1741 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index cc845afd4..67aac45c9 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -63,7 +63,38 @@ jobs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
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'
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 4c5db80c1..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
diff --git a/.vscode/launch.json b/.vscode/launch.json
index bf1bd65cb..05f60cfa6 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -11,7 +11,11 @@
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
"stopAtEntry": false,
- "internalConsoleOptions": "openOnSessionStart"
+ "internalConsoleOptions": "openOnSessionStart",
+ "serverReadyAction": {
+ "action": "openExternally",
+ "pattern": "Overriding address\\(es\\) \\'(https?:\\S+)\\'",
+ }
},
{
"name": ".NET Core Launch (nowebclient)",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f0724b412..99060d0b0 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -103,6 +103,7 @@
- [sl1288](https://github.com/sl1288)
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
+ - [spookbits](https://github.com/spookbits)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000)
@@ -135,6 +136,7 @@
- [YouKnowBlom](https://github.com/YouKnowBlom)
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
+ - [nielsvanvelzen](https://github.com/nielsvanvelzen)
# Emby Contributors
@@ -198,3 +200,4 @@
- [tikuf](https://github.com/tikuf/)
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
+ - [olsh](https://github.com/olsh)
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index d5629684c..1807ac6a1 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -126,32 +126,23 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
- builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
- builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
+ builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
+ builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
+ builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
+ builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
+ builder.Append("ModelName:").AppendLine(profile.ModelName);
+ builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
+ builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
+ builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
{
- if (!string.IsNullOrEmpty(profileInfo.DeviceDescription))
- {
- if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
- {
- return false;
- }
- }
-
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
{
- if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
+ if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
{
return false;
}
@@ -159,7 +150,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
{
- if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
+ if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
{
return false;
}
@@ -167,7 +158,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
{
- if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
+ if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
{
return false;
}
@@ -175,7 +166,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
{
- if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
+ if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
{
return false;
}
@@ -183,7 +174,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ModelName))
{
- if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
+ if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
{
return false;
}
@@ -191,7 +182,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
{
- if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
+ if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
{
return false;
}
@@ -199,7 +190,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
{
- if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
+ if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
{
return false;
}
@@ -207,7 +198,7 @@ namespace Emby.Dlna
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
{
- if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
+ if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
{
return false;
}
@@ -216,11 +207,11 @@ namespace Emby.Dlna
return true;
}
- private bool IsRegexMatch(string input, string pattern)
+ private bool IsRegexOrSubstringMatch(string input, string pattern)
{
try
{
- return Regex.IsMatch(input, pattern);
+ return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
catch (ArgumentException ex)
{
@@ -511,8 +502,7 @@ namespace Emby.Dlna
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
- var profile = GetProfile(headers) ??
- GetDefaultProfile();
+ var profile = GetDefaultProfile();
var serverId = _appHost.SystemId;
diff --git a/Emby.Dlna/Images/logo240.jpg b/Emby.Dlna/Images/logo240.jpg
index da1cb5e07..78a27f1b5 100644
--- a/Emby.Dlna/Images/logo240.jpg
+++ b/Emby.Dlna/Images/logo240.jpg
Binary files differ
diff --git a/Emby.Dlna/Images/people48.png b/Emby.Dlna/Images/people48.png
index 7fb25e6b3..dae5f6057 100644
--- a/Emby.Dlna/Images/people48.png
+++ b/Emby.Dlna/Images/people48.png
Binary files differ
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 328759c5b..63fd8ce5a 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -669,62 +669,57 @@ namespace Emby.Dlna.PlayTo
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{
- if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType))
- {
- switch (commandType)
- {
- case GeneralCommandType.VolumeDown:
- return _device.VolumeDown(cancellationToken);
- case GeneralCommandType.VolumeUp:
- return _device.VolumeUp(cancellationToken);
- case GeneralCommandType.Mute:
- return _device.Mute(cancellationToken);
- case GeneralCommandType.Unmute:
- return _device.Unmute(cancellationToken);
- case GeneralCommandType.ToggleMute:
- return _device.ToggleMute(cancellationToken);
- case GeneralCommandType.SetAudioStreamIndex:
- if (command.Arguments.TryGetValue("Index", out string index))
+ switch (command.Name)
+ {
+ case GeneralCommandType.VolumeDown:
+ return _device.VolumeDown(cancellationToken);
+ case GeneralCommandType.VolumeUp:
+ return _device.VolumeUp(cancellationToken);
+ case GeneralCommandType.Mute:
+ return _device.Mute(cancellationToken);
+ case GeneralCommandType.Unmute:
+ return _device.Unmute(cancellationToken);
+ case GeneralCommandType.ToggleMute:
+ return _device.ToggleMute(cancellationToken);
+ case GeneralCommandType.SetAudioStreamIndex:
+ if (command.Arguments.TryGetValue("Index", out string index))
+ {
+ if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
- {
- return SetAudioStreamIndex(val);
- }
-
- throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+ return SetAudioStreamIndex(val);
}
- throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
- case GeneralCommandType.SetSubtitleStreamIndex:
- if (command.Arguments.TryGetValue("Index", out index))
- {
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
- {
- return SetSubtitleStreamIndex(val);
- }
+ throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+ }
- throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+ throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
+ case GeneralCommandType.SetSubtitleStreamIndex:
+ if (command.Arguments.TryGetValue("Index", out index))
+ {
+ if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ {
+ return SetSubtitleStreamIndex(val);
}
- throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
- case GeneralCommandType.SetVolume:
- if (command.Arguments.TryGetValue("Volume", out string vol))
- {
- if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
- {
- return _device.SetVolume(volume, cancellationToken);
- }
+ throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+ }
- throw new ArgumentException("Unsupported volume value supplied.");
+ throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
+ case GeneralCommandType.SetVolume:
+ if (command.Arguments.TryGetValue("Volume", out string vol))
+ {
+ if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+ {
+ return _device.SetVolume(volume, cancellationToken);
}
- throw new ArgumentException("Volume argument cannot be null");
- default:
- return Task.CompletedTask;
- }
- }
+ throw new ArgumentException("Unsupported volume value supplied.");
+ }
- return Task.CompletedTask;
+ throw new ArgumentException("Volume argument cannot be null");
+ default:
+ return Task.CompletedTask;
+ }
}
private async Task SetAudioStreamIndex(int? newIndex)
@@ -886,7 +881,10 @@ namespace Emby.Dlna.PlayTo
return null;
}
- mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
+ if (_mediaSourceManager != null)
+ {
+ mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
+ }
return mediaSource;
}
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index 3d1dd3e73..21877f121 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -130,25 +130,21 @@ namespace Emby.Dlna.PlayTo
}
}
- private string GetUuid(string usn)
+ private static string GetUuid(string usn)
{
- var found = false;
- var index = usn.IndexOf("uuid:", StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- usn = usn.Substring(index);
- found = true;
- }
+ const string UuidStr = "uuid:";
+ const string UuidColonStr = "::";
- index = usn.IndexOf("::", StringComparison.OrdinalIgnoreCase);
+ var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
- usn = usn.Substring(0, index);
+ return usn.Substring(index + UuidStr.Length);
}
- if (found)
+ index = usn.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
{
- return usn;
+ usn = usn.Substring(0, index + UuidColonStr.Length);
}
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index 8683c8997..c8c36fc97 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -101,7 +101,7 @@ namespace Emby.Dlna.PlayTo
LoadOptions.PreserveWhitespace);
}
- private Task<HttpResponseMessage> PostSoapDataAsync(
+ private async Task<HttpResponseMessage> PostSoapDataAsync(
string url,
string soapAction,
string postData,
@@ -126,7 +126,7 @@ namespace Emby.Dlna.PlayTo
options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
- return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index bca9e81cd..1f429d0de 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))
+ .Append(BuildUrl(service.ScpdUrl, true))
.Append("</SCPDURL>");
builder.Append("<controlURL>")
- .Append(BuildUrl(service.ControlUrl))
+ .Append(BuildUrl(service.ControlUrl, true))
.Append("</controlURL>");
builder.Append("<eventSubURL>")
- .Append(BuildUrl(service.EventSubUrl))
+ .Append(BuildUrl(service.EventSubUrl, true))
.Append("</eventSubURL>");
builder.Append("</service>");
@@ -250,7 +250,13 @@ namespace Emby.Dlna.Server
builder.Append("</serviceList>");
}
- private string BuildUrl(string url)
+ /// <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)
{
if (string.IsNullOrEmpty(url))
{
@@ -261,7 +267,7 @@ namespace Emby.Dlna.Server
url = "/dlna/" + _serverUdn + "/" + url;
- if (EnableAbsoluteUrls)
+ if (EnableAbsoluteUrls || absoluteUrl)
{
url = _serverAddress.TrimEnd('/') + url;
}
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index f585b90ca..ed20292f6 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -455,7 +455,7 @@ namespace Emby.Drawing
throw new ArgumentException("Path can't be empty.", nameof(path));
}
- if (path.IsEmpty)
+ if (filename.IsEmpty)
{
throw new ArgumentException("Filename can't be empty.", nameof(filename));
}
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
index 3c874c62c..14edd6492 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
@@ -1,6 +1,6 @@
+#nullable enable
#pragma warning disable CS1591
-using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@@ -19,12 +19,7 @@ namespace Emby.Naming.AudioBook
public AudioBookFilePathParserResult Parse(string path)
{
- if (path == null)
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- var result = new AudioBookFilePathParserResult();
+ AudioBookFilePathParserResult result = default;
var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions)
{
@@ -50,27 +45,14 @@ namespace Emby.Naming.AudioBook
{
if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
- result.ChapterNumber = intValue;
+ result.PartNumber = intValue;
}
}
}
}
}
- /*var matches = _iRegexProvider.GetRegex("\\d+", RegexOptions.IgnoreCase).Matches(fileName);
- if (matches.Count > 0)
- {
- if (!result.ChapterNumber.HasValue)
- {
- result.ChapterNumber = int.Parse(matches[0].Groups[0].Value);
- }
-
- if (matches.Count > 1)
- {
- result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value);
- }
- }*/
- result.Success = result.PartNumber.HasValue || result.ChapterNumber.HasValue;
+ result.Success = result.ChapterNumber.HasValue || result.PartNumber.HasValue;
return result;
}
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
index e28a58db7..7bfc4479d 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
@@ -1,8 +1,9 @@
+#nullable enable
#pragma warning disable CS1591
namespace Emby.Naming.AudioBook
{
- public class AudioBookFilePathParserResult
+ public struct AudioBookFilePathParserResult
{
public int? PartNumber { get; set; }
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index 5466b4637..ed53bd04f 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -55,8 +55,8 @@ namespace Emby.Naming.AudioBook
{
Path = path,
Container = container,
- PartNumber = parsingResult.PartNumber,
ChapterNumber = parsingResult.ChapterNumber,
+ PartNumber = parsingResult.PartNumber,
IsDirectory = isDirectory
};
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index c37e87d96..7a46fdf2e 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -4,6 +4,7 @@ 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;
@@ -37,6 +38,7 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
@@ -119,6 +121,7 @@ namespace Emby.Server.Implementations
private readonly IFileSystem _fileSystemManager;
private readonly INetworkManager _networkManager;
private readonly IXmlSerializer _xmlSerializer;
+ private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private IMediaEncoder _mediaEncoder;
@@ -238,8 +241,14 @@ namespace Emby.Server.Implementations
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
/// <summary>
- /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
+ /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
/// </summary>
+ /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
@@ -249,6 +258,8 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
+ _jsonSerializer = new JsonSerializer();
+
ServiceCollection = serviceCollection;
_networkManager = networkManager;
@@ -1016,6 +1027,119 @@ 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)
+ {
+ var dllList = new List<string>();
+ var versions = new List<(Version PluginVersion, string Name, string Path)>();
+ var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
+ string metafile;
+
+ foreach (var dir in directories)
+ {
+ try
+ {
+ 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);
+ }
+
+ if (!Version.TryParse(manifest.Version, out var version))
+ {
+ version = new Version(0, 0, 0, 1);
+ }
+
+ if (ApplicationVersion >= targetAbi)
+ {
+ // Only load Plugins if the plugin is built for this version or below.
+ versions.Add((version, manifest.Name, 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))
+ {
+ // Versioned folder.
+ versions.Add((ver, metafile, 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));
+ }
+ }
+ }
+ catch
+ {
+ continue;
+ }
+ }
+
+ string lastName = string.Empty;
+ versions.Sort(VersionCompare);
+ // 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));
+ lastName = versions[x].Name;
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(lastName) && cleanup)
+ {
+ // Attempt a cleanup of old folders.
+ try
+ {
+ Logger.LogDebug("Deleting {Path}", versions[x].Path);
+ Directory.Delete(versions[x].Path, true);
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
+ }
+ }
+ }
+
+ return dllList;
+ }
+
+ /// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
@@ -1023,7 +1147,7 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
- foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
+ foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
{
Assembly plugAss;
try
@@ -1137,7 +1261,8 @@ namespace Emby.Server.Implementations
Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
- LocalAddress = localAddress
+ LocalAddress = localAddress,
+ StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
@@ -1431,10 +1556,6 @@ namespace Emby.Server.Implementations
}
}
- public virtual void EnableLoopback(string appName)
- {
- }
-
private bool _disposed = false;
/// <summary>
diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
deleted file mode 100644
index f8108d1c2..000000000
--- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Browser
-{
- /// <summary>
- /// Assists in opening application URLs in an external browser.
- /// </summary>
- public static class BrowserLauncher
- {
- /// <summary>
- /// Opens the home page of the web client.
- /// </summary>
- /// <param name="appHost">The app host.</param>
- public static void OpenWebApp(IServerApplicationHost appHost)
- {
- TryOpenUrl(appHost, "/web/index.html");
- }
-
- /// <summary>
- /// Opens the swagger API page.
- /// </summary>
- /// <param name="appHost">The app host.</param>
- public static void OpenSwaggerPage(IServerApplicationHost appHost)
- {
- TryOpenUrl(appHost, "/api-docs/swagger");
- }
-
- /// <summary>
- /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
- /// </summary>
- /// <param name="appHost">The application host.</param>
- /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param>
- private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl)
- {
- try
- {
- string baseUrl = appHost.GetLocalApiUrl("localhost");
- appHost.LaunchUrl(baseUrl + relativeUrl);
- }
- catch (Exception ex)
- {
- var logger = appHost.Resolve<ILogger<IServerApplicationHost>>();
- logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index fde6fa115..cd9dbb1bd 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations
{ DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
- { PlaylistsAllowDuplicatesKey, bool.TrueString },
+ { PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString }
};
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 8a3716380..0fb050a7a 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -143,8 +143,17 @@ namespace Emby.Server.Implementations.Data
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(sql);
- public IEnumerable<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
- => sql.Select(connection.PrepareStatement);
+ public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
+ {
+ int len = sql.Count;
+ IStatement[] statements = new IStatement[len];
+ for (int i = 0; i < len; i++)
+ {
+ statements[i] = connection.PrepareStatement(sql[i]);
+ }
+
+ return statements;
+ }
protected bool TableExists(ManagedConnection connection, string name)
{
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 716e5071d..70a6df977 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -234,7 +234,9 @@ namespace Emby.Server.Implementations.Data
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
- bindParam.Bind(value.ToByteArray());
+ Span<byte> byteValue = stackalloc byte[16];
+ value.TryWriteBytes(byteValue);
+ bindParam.Bind(byteValue);
}
else
{
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 5bf740cfc..d09f84e17 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -138,7 +138,6 @@ namespace Emby.Server.Implementations.Data
"pragma shrink_memory"
};
-
string[] postQueries =
{
// obsolete
@@ -560,7 +559,7 @@ namespace Emby.Server.Implementations.Data
{
SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId"
- }).ToList();
+ });
using (var saveItemStatement = statements[0])
using (var deleteAncestorsStatement = statements[1])
@@ -2264,7 +2263,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
}
-
private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
@@ -2925,7 +2923,7 @@ namespace Emby.Server.Implementations.Data
{
connection.RunInTransaction(db =>
{
- var statements = PrepareAll(db, statementTexts).ToList();
+ var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@@ -2963,7 +2961,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount)
{
- using (var statement = statements[statements.Count - 1])
+ using (var statement = statements[statements.Length - 1])
{
if (EnableJoinUserData(query))
{
@@ -3292,7 +3290,6 @@ namespace Emby.Server.Implementations.Data
}
}
-
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
var statementTexts = new List<string>();
@@ -3329,7 +3326,7 @@ namespace Emby.Server.Implementations.Data
{
connection.RunInTransaction(db =>
{
- var statements = PrepareAll(db, statementTexts).ToList();
+ var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@@ -3355,7 +3352,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount)
{
- using (var statement = statements[statements.Count - 1])
+ using (var statement = statements[statements.Length - 1])
{
if (EnableJoinUserData(query))
{
@@ -3718,26 +3715,31 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
}
+ StringBuilder clauseBuilder = new StringBuilder();
+ const string Or = " OR ";
+
var trailerTypes = query.TrailerTypes;
int trailerTypesLen = trailerTypes.Length;
if (trailerTypesLen > 0)
{
- const string Or = " OR ";
- StringBuilder clause = new StringBuilder("(", trailerTypesLen * 32);
+ clauseBuilder.Append('(');
+
for (int i = 0; i < trailerTypesLen; i++)
{
var paramName = "@TrailerTypes" + i;
- clause.Append("TrailerTypes like ")
+ clauseBuilder.Append("TrailerTypes like ")
.Append(paramName)
.Append(Or);
statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
}
// Remove last " OR "
- clause.Length -= Or.Length;
- clause.Append(')');
+ clauseBuilder.Length -= Or.Length;
+ clauseBuilder.Append(')');
+
+ whereClauses.Add(clauseBuilder.ToString());
- whereClauses.Add(clause.ToString());
+ clauseBuilder.Length = 0;
}
if (query.IsAiring.HasValue)
@@ -3757,23 +3759,35 @@ namespace Emby.Server.Implementations.Data
}
}
- if (query.PersonIds.Length > 0)
+ int personIdsLen = query.PersonIds.Length;
+ if (personIdsLen > 0)
{
// TODO: Should this query with CleanName ?
- var clauses = new List<string>();
- var index = 0;
- foreach (var personId in query.PersonIds)
+ clauseBuilder.Append('(');
+
+ Span<byte> idBytes = stackalloc byte[16];
+ for (int i = 0; i < personIdsLen; i++)
{
- var paramName = "@PersonId" + index;
+ string paramName = "@PersonId" + i;
+ clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=")
+ .Append(paramName)
+ .Append("))) OR ");
- clauses.Add("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=" + paramName + ")))");
- statement?.TryBind(paramName, personId.ToByteArray());
- index++;
+ if (statement != null)
+ {
+ query.PersonIds[i].TryWriteBytes(idBytes);
+ statement.TryBind(paramName, idBytes);
+ }
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ // Remove last " OR "
+ clauseBuilder.Length -= Or.Length;
+ clauseBuilder.Append(')');
+
+ whereClauses.Add(clauseBuilder.ToString());
+
+ clauseBuilder.Length = 0;
}
if (!string.IsNullOrWhiteSpace(query.Person))
@@ -5149,7 +5163,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
- var itemIdBlob = itemId.ToByteArray();
+ Span<byte> itemIdBlob = stackalloc byte[16];
+ itemId.TryWriteBytes(itemIdBlob);
// First delete
deleteAncestorsStatement.Reset();
@@ -5165,17 +5180,15 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
for (var i = 0; i < ancestorIds.Count; i++)
{
- if (i > 0)
- {
- insertText.Append(',');
- }
-
insertText.AppendFormat(
CultureInfo.InvariantCulture,
- "(@ItemId, @AncestorId{0}, @AncestorIdText{0})",
+ "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),",
i.ToString(CultureInfo.InvariantCulture));
}
+ // Remove last ,
+ insertText.Length--;
+
using (var statement = PrepareStatement(db, insertText.ToString()))
{
statement.TryBind("@ItemId", itemIdBlob);
@@ -5185,8 +5198,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
var index = i.ToString(CultureInfo.InvariantCulture);
var ancestorId = ancestorIds[i];
+ ancestorId.TryWriteBytes(itemIdBlob);
- statement.TryBind("@AncestorId" + index, ancestorId.ToByteArray());
+ statement.TryBind("@AncestorId" + index, itemIdBlob);
statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
}
@@ -5466,7 +5480,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts).ToList();
+ var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@@ -5517,7 +5531,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
+ GetJoinUserDataText(query)
+ whereText;
- using (var statement = statements[statements.Count - 1])
+ using (var statement = statements[statements.Length - 1])
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5990,7 +6004,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
-
/// <summary>
/// Gets the chapter.
/// </summary>
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 57c1398e9..edb8753fd 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -468,7 +468,6 @@ namespace Emby.Server.Implementations.Dto
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
Name = item.Album,
Limit = 1
-
});
if (parentAlbumIds.Count > 0)
@@ -1139,6 +1138,7 @@ namespace Emby.Server.Implementations.Dto
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
+ AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
}
@@ -1185,6 +1185,7 @@ namespace Emby.Server.Implementations.Dto
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
+ AttachPrimaryImageAspectRatio(dto, series);
}
}
}
@@ -1431,7 +1432,7 @@ namespace Emby.Server.Implementations.Dto
return null;
}
- return width / height;
+ return (double)width / height;
}
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 0a348f0d0..9ed3cca99 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,7 +22,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="IPNetwork2" Version="2.5.224" />
+ <PackageReference Include="IPNetwork2" Version="2.5.226" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
@@ -32,11 +32,11 @@
<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.7" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
- <PackageReference Include="Mono.Nat" Version="2.0.2" />
+ <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="Mono.Nat" Version="3.0.0" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
<PackageReference Include="sharpcompress" Version="0.26.0" />
diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs
deleted file mode 100644
index 2e738deeb..000000000
--- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Browser;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Configuration;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
- /// <summary>
- /// Class StartupWizard.
- /// </summary>
- public sealed class StartupWizard : IServerEntryPoint
- {
- private readonly IServerApplicationHost _appHost;
- private readonly IConfiguration _appConfig;
- private readonly IServerConfigurationManager _config;
- private readonly IStartupOptions _startupOptions;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StartupWizard"/> class.
- /// </summary>
- /// <param name="appHost">The application host.</param>
- /// <param name="appConfig">The application configuration.</param>
- /// <param name="config">The configuration manager.</param>
- /// <param name="startupOptions">The application startup options.</param>
- public StartupWizard(
- IServerApplicationHost appHost,
- IConfiguration appConfig,
- IServerConfigurationManager config,
- IStartupOptions startupOptions)
- {
- _appHost = appHost;
- _appConfig = appConfig;
- _config = config;
- _startupOptions = startupOptions;
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- Run();
- return Task.CompletedTask;
- }
-
- private void Run()
- {
- if (!_appHost.CanLaunchWebBrowser)
- {
- return;
- }
-
- // Always launch the startup wizard if possible when it has not been completed
- if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient())
- {
- BrowserLauncher.OpenWebApp(_appHost);
- return;
- }
-
- // Do nothing if the web app is configured to not run automatically
- if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp)
- {
- return;
- }
-
- // Launch the swagger page if the web client is not hosted, otherwise open the web client
- if (_appConfig.HostWebClient())
- {
- BrowserLauncher.OpenWebApp(_appHost);
- }
- else
- {
- BrowserLauncher.OpenSwaggerPage(_appHost);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- }
- }
-}
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 3618b88c5..1da717e75 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -28,7 +28,6 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly object _syncLock = new object();
private Timer _updateTimer;
-
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
{
_userDataManager = userDataManager;
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 8777c59b7..86914dea2 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
- return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user);
+ return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
}
public SessionInfo GetSession(object requestContext)
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index e7e72c686..4bef59543 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -17,11 +17,6 @@ namespace Emby.Server.Implementations
bool IsService { get; }
/// <summary>
- /// Gets the value of the --noautorunwebapp command line option.
- /// </summary>
- bool NoAutoRunWebApp { get; }
-
- /// <summary>
/// Gets the value of the --package-name command line option.
/// </summary>
string PackageName { get; }
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 375f09f5b..00282b71a 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -513,10 +513,11 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(type));
}
- if (key.StartsWith(_configurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal))
+ string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
+ if (key.StartsWith(programDataPath, StringComparison.Ordinal))
{
// Try to normalize paths located underneath program-data in an attempt to make them more portable
- key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
+ key = key.Substring(programDataPath.Length)
.TrimStart('/', '\\')
.Replace('/', '\\');
}
@@ -871,17 +872,17 @@ namespace Emby.Server.Implementations.Library
public Guid GetStudioId(string name)
{
- return GetItemByNameId<Studio>(Studio.GetPath, name);
+ return GetItemByNameId<Studio>(Studio.GetPath(name));
}
public Guid GetGenreId(string name)
{
- return GetItemByNameId<Genre>(Genre.GetPath, name);
+ return GetItemByNameId<Genre>(Genre.GetPath(name));
}
public Guid GetMusicGenreId(string name)
{
- return GetItemByNameId<MusicGenre>(MusicGenre.GetPath, name);
+ return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
}
/// <summary>
@@ -943,7 +944,7 @@ namespace Emby.Server.Implementations.Library
{
var existing = GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(T).Name },
+ IncludeItemTypes = new[] { nameof(MusicArtist) },
Name = name,
DtoOptions = options
}).Cast<MusicArtist>()
@@ -957,13 +958,11 @@ namespace Emby.Server.Implementations.Library
}
}
- var id = GetItemByNameId<T>(getPathFn, name);
-
+ var path = getPathFn(name);
+ var id = GetItemByNameId<T>(path);
var item = GetItemById(id) as T;
-
if (item == null)
{
- var path = getPathFn(name);
item = new T
{
Name = name,
@@ -979,10 +978,9 @@ namespace Emby.Server.Implementations.Library
return item;
}
- private Guid GetItemByNameId<T>(Func<string, string> getPathFn, string name)
+ private Guid GetItemByNameId<T>(string path)
where T : BaseItem, new()
{
- var path = getPathFn(name);
var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
}
@@ -1805,21 +1803,18 @@ namespace Emby.Server.Implementations.Library
/// <param name="items">The items.</param>
/// <param name="parent">The parent item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
+ public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
{
- // Don't iterate multiple times
- var itemsList = items.ToList();
-
- _itemRepository.SaveItems(itemsList, cancellationToken);
+ _itemRepository.SaveItems(items, cancellationToken);
- foreach (var item in itemsList)
+ foreach (var item in items)
{
RegisterItem(item);
}
if (ItemAdded != null)
{
- foreach (var item in itemsList)
+ foreach (var item in items)
{
// With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library)
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index f9ae55af8..28aabc159 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -468,13 +468,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
imageIdString = imageIdString.TrimEnd(',') + "]";
- using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs");
- message.Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json);
+ using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs")
+ {
+ Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json)
+ };
try
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
- await using var response = await innerResponse2.Content.ReadAsStreamAsync();
+ await using var response = await innerResponse2.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
response).ConfigureAwait(false);
}
@@ -872,7 +874,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public List<Lineup> lineups { get; set; }
}
-
public class Headends
{
public string headend { get; set; }
@@ -884,8 +885,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public List<Lineup> lineups { get; set; }
}
-
-
public class Map
{
public string stationID { get; set; }
@@ -969,9 +968,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public List<string> date { get; set; }
}
-
-
-
public class Rating
{
public string body { get; set; }
@@ -1015,8 +1011,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public string isPremiereOrFinale { get; set; }
}
-
-
public class MetadataSchedule
{
public string modified { get; set; }
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index a898a564f..5bdd1c23c 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -2135,6 +2135,7 @@ namespace Emby.Server.Implementations.LiveTv
}
private bool _disposed = false;
+
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 28e30fac8..2f4c60117 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -563,6 +563,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
+ var tunerCount = info.TunerCount;
+
+ if (tunerCount > 0)
+ {
+ var tunerHostId = info.Id;
+ var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
+
+ if (liveStreams.Count() >= tunerCount)
+ {
+ throw new LiveTvConflictException("HDHomeRun simultaneous stream limit has been reached.");
+ }
+ }
+
var profile = streamId.Split('_')[0];
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile);
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index e587c37d5..d33e11893 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -1,19 +1,19 @@
{
"Artists": "Kunstenare",
"Channels": "Kanale",
- "Folders": "Fouers",
- "Favorites": "Gunstelinge",
+ "Folders": "Lêergidse",
+ "Favorites": "Gunstellinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
- "Movies": "Rolprente",
- "Shows": "Program",
- "HeaderContinueWatching": "Hou Aan Kyk",
+ "Movies": "Flieks",
+ "Shows": "Televisie Reekse",
+ "HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Fotos",
- "Playlists": "Speellysse",
+ "Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer",
@@ -23,7 +23,7 @@
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
- "HeaderLiveTV": "Live TV",
+ "HeaderLiveTV": "Lewendige TV",
"Application": "Program",
"AppDeviceValues": "App: {0}, Toestel: {1}",
"VersionNumber": "Weergawe {0}",
@@ -95,5 +95,23 @@
"TasksChannelsCategory": "Internet kanale",
"TasksApplicationCategory": "aansoek",
"TasksLibraryCategory": "biblioteek",
- "TasksMaintenanceCategory": "onderhoud"
+ "TasksMaintenanceCategory": "onderhoud",
+ "TaskCleanCacheDescription": "Vee kasregister lêers uit wat nie meer deur die stelsel benodig word nie.",
+ "TaskCleanCache": "Reinig Kasgeheue Lêergids",
+ "TaskDownloadMissingSubtitlesDescription": "Soek aanlyn vir vermiste onderskrifte gebasseer op metadata verstellings.",
+ "TaskDownloadMissingSubtitles": "Laai vermiste onderskrifte af",
+ "TaskRefreshChannelsDescription": "Vervris internet kanaal inligting.",
+ "TaskRefreshChannels": "Vervris Kanale",
+ "TaskCleanTranscodeDescription": "Vee transkodering lêers uit wat ouer is as een dag.",
+ "TaskCleanTranscode": "Reinig Transkoderings Leêrbinder",
+ "TaskUpdatePluginsDescription": "Laai opgedateerde inprop-sagteware af en installeer inprop-sagteware wat verstel is om outomaties op te dateer.",
+ "TaskUpdatePlugins": "Dateer Inprop-Sagteware Op",
+ "TaskRefreshPeopleDescription": "Vervris metadata oor akteurs en regisseurs in u media versameling.",
+ "TaskRefreshPeople": "Vervris Mense",
+ "TaskCleanLogsDescription": "Vee loglêers wat ouer as {0} dae is uit.",
+ "TaskCleanLogs": "Reinig Loglêer Lêervouer",
+ "TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
+ "TaskRefreshLibrary": "Skandeer Media Versameling",
+ "TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
+ "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 47ebe1254..7fc996821 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -107,7 +107,7 @@
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
- "TaskRefreshLibrary": "Scanner toute les Bibliothèques",
+ "TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
"TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index 94034962d..faee2519a 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -1,3 +1,11 @@
{
- "Albums": "Álbumes"
+ "Albums": "Álbumes",
+ "Collections": "Colecións",
+ "ChapterNameValue": "Capítulos {0}",
+ "Channels": "Canles",
+ "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
+ "Books": "Libros",
+ "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
+ "Artists": "Artistas",
+ "Application": "Aplicativo"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index b0dfc312e..585fc6f02 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -19,7 +19,7 @@
"HeaderFavoriteEpisodes": "Episode Favorit",
"HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit",
- "HeaderContinueWatching": "Lanjutkan Menonton",
+ "HeaderContinueWatching": "Lanjut Menonton",
"HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis",
"Genres": "Aliran",
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 9e3ecd5a8..a33953c27 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -84,8 +84,8 @@
"UserDeletedWithName": "사용자 {0} 삭제됨",
"UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다",
"UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다",
- "UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다",
- "UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다",
+ "UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴",
+ "UserOnlineFromDevice": "{0}이 {1}으로 접속",
"UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다",
"UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다",
"UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index a97c2e17a..07a599121 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Lydavspilling startet",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
"NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
- "NotificationOptionInstallationFailed": "Installasjonsfeil",
+ "NotificationOptionInstallationFailed": "Installasjonen feilet",
"NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
"NotificationOptionPluginError": "Pluginfeil",
"NotificationOptionPluginInstalled": "Plugin installert",
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
new file mode 100644
index 000000000..347ba5f97
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -0,0 +1,117 @@
+{
+ "MessageApplicationUpdatedTo": "Serveri Jellyfin u përditesua në versionin {0}",
+ "Inherit": "Trashgimi",
+ "TaskDownloadMissingSubtitlesDescription": "Kërkon në internet për titra që mungojnë bazuar tek konfigurimi i metadata-ve.",
+ "TaskDownloadMissingSubtitles": "Shkarko titra që mungojnë",
+ "TaskRefreshChannelsDescription": "Rifreskon informacionin e kanaleve të internetit.",
+ "TaskRefreshChannels": "Rifresko Kanalet",
+ "TaskCleanTranscodeDescription": "Fshin skedarët e transkodimit që janë më të vjetër se një ditë.",
+ "TaskCleanTranscode": "Fshi dosjen e transkodimit",
+ "TaskUpdatePluginsDescription": "Shkarkon dhe instalon përditësimi për plugin që janë konfiguruar të përditësohen automatikisht.",
+ "TaskUpdatePlugins": "Përditëso Plugin",
+ "TaskRefreshPeopleDescription": "Përditëson metadata të aktorëve dhe regjizorëve në librarinë tuaj.",
+ "TaskRefreshPeople": "Rifresko aktorët",
+ "TaskCleanLogsDescription": "Fshin skëdarët log që janë më të vjetër se {0} ditë.",
+ "TaskCleanLogs": "Fshi dosjen Log",
+ "TaskRefreshLibraryDescription": "Skanon librarinë media për skedarë të rinj dhe rifreskon metadata.",
+ "TaskRefreshLibrary": "Skano librarinë media",
+ "TaskRefreshChapterImagesDescription": "Krijon imazh për videot që kanë kapituj.",
+ "TaskRefreshChapterImages": "Ekstrakto Imazhet e Kapitullit",
+ "TaskCleanCacheDescription": "Fshi skedarët e cache-s që nuk i duhen më sistemit.",
+ "TaskCleanCache": "Pastro memorjen cache",
+ "TasksChannelsCategory": "Kanalet nga interneti",
+ "TasksApplicationCategory": "Aplikacioni",
+ "TasksLibraryCategory": "Libraria",
+ "TasksMaintenanceCategory": "Mirëmbajtje",
+ "VersionNumber": "Versioni {0}",
+ "ValueSpecialEpisodeName": "Speciale - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} u shtua tek libraria juaj",
+ "UserStoppedPlayingItemWithValues": "{0} mbaroi së shikuari {1} tek {2}",
+ "UserStartedPlayingItemWithValues": "{0} po shikon {1} tek {2}",
+ "UserPolicyUpdatedWithName": "Politika e përdoruesit u përditësua për {0}",
+ "UserPasswordChangedWithName": "Fjalëkalimi u ndryshua për përdoruesin {0}",
+ "UserOnlineFromDevice": "{0} është në linjë nga {1}",
+ "UserOfflineFromDevice": "{0} u shkëput nga {1}",
+ "UserLockedOutWithName": "Përdoruesi {0} u përjashtua",
+ "UserDownloadingItemWithValues": "{0} po shkarkon {1}",
+ "UserDeletedWithName": "Përdoruesi {0} u fshi",
+ "UserCreatedWithName": "Përdoruesi {0} u krijua",
+ "User": "Përdoruesi",
+ "TvShows": "Seriale TV",
+ "System": "Sistemi",
+ "Sync": "Sinkronizo",
+ "SubtitleDownloadFailureFromForItem": "Titrat deshtuan të shkarkohen nga {0} për {1}",
+ "StartupEmbyServerIsLoading": "Serveri Jellyfin po ngarkohet. Ju lutemi provoni përseri pas pak.",
+ "Songs": "Këngë",
+ "Shows": "Seriale",
+ "ServerNameNeedsToBeRestarted": "{0} duhet të ristartoj",
+ "ScheduledTaskStartedWithName": "{0} filloi",
+ "ScheduledTaskFailedWithName": "{0} dështoi",
+ "ProviderValue": "Ofruesi: {0}",
+ "PluginUpdatedWithName": "{0} u përditësua",
+ "PluginUninstalledWithName": "{0} u çinstalua",
+ "PluginInstalledWithName": "{0} u instalua",
+ "Plugin": "Plugin",
+ "Playlists": "Listat për luajtje",
+ "Photos": "Fotografitë",
+ "NotificationOptionVideoPlaybackStopped": "Luajtja e videos ndaloi",
+ "NotificationOptionVideoPlayback": "Luajtja e videos filloi",
+ "NotificationOptionUserLockedOut": "Përdoruesi u përjashtua",
+ "NotificationOptionTaskFailed": "Ushtrimi i planifikuar dështoi",
+ "NotificationOptionServerRestartRequired": "Kërkohet ristartim i serverit",
+ "NotificationOptionPluginUpdateInstalled": "Përditësimi i plugin u instalua",
+ "NotificationOptionPluginUninstalled": "Plugin u çinstalua",
+ "NotificationOptionPluginInstalled": "Plugin u instalua",
+ "NotificationOptionPluginError": "Plugin dështoi",
+ "NotificationOptionNewLibraryContent": "Një përmbajtje e re u shtua",
+ "NotificationOptionInstallationFailed": "Instalimi dështoi",
+ "NotificationOptionCameraImageUploaded": "Fotoja nga kamera u ngarkua",
+ "NotificationOptionAudioPlaybackStopped": "Luajtja e audios ndaloi",
+ "NotificationOptionAudioPlayback": "Luajtja e audios filloi",
+ "NotificationOptionApplicationUpdateInstalled": "Përditësimi i aplikacionit u instalua",
+ "NotificationOptionApplicationUpdateAvailable": "Një perditësim i aplikacionit është gati",
+ "NewVersionIsAvailable": "Një version i ri i Jellyfin është gati për tu shkarkuar.",
+ "NameSeasonUnknown": "Sezon i panjohur",
+ "NameSeasonNumber": "Sezoni {0}",
+ "NameInstallFailed": "Instalimi i {0} dështoi",
+ "MusicVideos": "Video muzikore",
+ "Music": "Muzikë",
+ "Movies": "Filma",
+ "MixedContent": "Përmbajtje e përzier",
+ "MessageServerConfigurationUpdated": "Konfigurimet e serverit u përditësuan",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Seksioni i konfigurimit të serverit {0} u përditësua",
+ "MessageApplicationUpdated": "Serveri Jellyfin u përditësua",
+ "Latest": "Të fundit",
+ "LabelRunningTimeValue": "Kohëzgjatja: {0}",
+ "LabelIpAddressValue": "Adresa IP: {0}",
+ "ItemRemovedWithName": "{0} u fshi nga libraria",
+ "ItemAddedWithName": "{0} u shtua tek libraria",
+ "HomeVideos": "Video personale",
+ "HeaderRecordingGroups": "Grupet e regjistrimit",
+ "HeaderNextUp": "Në vazhdim",
+ "HeaderLiveTV": "TV Live",
+ "HeaderFavoriteSongs": "Kënget e preferuara",
+ "HeaderFavoriteShows": "Serialet e preferuar",
+ "HeaderFavoriteEpisodes": "Episodet e preferuar",
+ "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",
+ "Favorites": "Të preferuara",
+ "FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}",
+ "DeviceOnlineWithName": "{0} u lidh",
+ "DeviceOfflineWithName": "{0} u shkëput",
+ "Collections": "Koleksione",
+ "ChapterNameValue": "Kapituj",
+ "Channels": "Kanale",
+ "CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}",
+ "Books": "Libra",
+ "AuthenticationSucceededWithUserName": "{0} u identifikua me sukses",
+ "Artists": "Artistë",
+ "Application": "Aplikacioni",
+ "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
+ "Albums": "Albumet"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index ed6877f7d..810b1b9ab 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -26,7 +26,7 @@
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
"DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
"Collections": "தொகுப்புகள்",
- "CameraImageUploadedFrom": "{0} இலிருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
+ "CameraImageUploadedFrom": "{0} இல் இருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
"AppDeviceValues": "செயலி: {0}, சாதனம்: {1}",
"TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
"TaskRefreshChannels": "சேனல்களை புதுப்பி",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 3f6f3b23c..3b77215a3 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -20,8 +20,8 @@
"NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว",
"NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง",
"NotificationOptionAudioPlayback": "เริ่มเล่นเสียง",
- "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอพพลิเคชันแล้ว",
- "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอพพลิเคชัน",
+ "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอปพลิเคชันแล้ว",
+ "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอปพลิเคชัน",
"NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
"NameSeasonUnknown": "ไม่ทราบซีซัน",
"NameSeasonNumber": "ซีซัน {0}",
@@ -65,8 +65,8 @@
"Books": "หนังสือ",
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
"Artists": "ศิลปิน",
- "Application": "แอพพลิเคชัน",
- "AppDeviceValues": "แอพ: {0}, อุปกรณ์: {1}",
+ "Application": "แอปพลิเคชัน",
+ "AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}",
"Albums": "อัลบั้ม",
"ScheduledTaskStartedWithName": "{0} เริ่มต้น",
"ScheduledTaskFailedWithName": "{0} ล้มเหลว",
@@ -92,7 +92,7 @@
"TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
"TaskCleanCache": "ล้างไดเรกทอรีแคช",
"TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
- "TasksApplicationCategory": "แอพพลิเคชัน",
+ "TasksApplicationCategory": "แอปพลิเคชัน",
"TasksLibraryCategory": "ไลบรารี",
"TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
"VersionNumber": "เวอร์ชัน {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
new file mode 100644
index 000000000..2392c8347
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -0,0 +1,117 @@
+{
+ "Collections": "Bộ Sưu Tập",
+ "Favorites": "Yêu Thích",
+ "Folders": "Thư Mục",
+ "Genres": "Thể Loại",
+ "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
+ "HeaderContinueWatching": "Xem Tiếp",
+ "HeaderLiveTV": "TV Trực Tiếp",
+ "Movies": "Phim",
+ "Photos": "Ảnh",
+ "Playlists": "Danh sách phát",
+ "Shows": "Chương Trình TV",
+ "Songs": "Các Bài Hát",
+ "Sync": "Đồng Bộ",
+ "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
+ "Albums": "Albums",
+ "Artists": "Các Nghệ Sĩ",
+ "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
+ "TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu",
+ "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
+ "TaskRefreshChannels": "Làm Mới Kênh",
+ "TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
+ "TaskCleanTranscode": "Làm Sạch Thư Mục Chuyển Mã",
+ "TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
+ "TaskUpdatePlugins": "Cập Nhật Plugins",
+ "TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
+ "TaskRefreshPeople": "Làm mới Người dùng",
+ "TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
+ "TaskCleanLogs": "Làm sạch nhật ký",
+ "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.",
+ "TaskRefreshLibrary": "Quét Thư viện Phương tiện",
+ "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
+ "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
+ "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
+ "TaskCleanCache": "Làm Sạch Thư Mục Cache",
+ "TasksChannelsCategory": "Kênh Internet",
+ "TasksApplicationCategory": "Ứng Dụng",
+ "TasksLibraryCategory": "Thư Viện",
+ "TasksMaintenanceCategory": "Bảo Trì",
+ "VersionNumber": "Phiên Bản {0}",
+ "ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
+ "UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
+ "UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
+ "UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
+ "UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
+ "UserOnlineFromDevice": "{0} trực tuyến từ {1}",
+ "UserOfflineFromDevice": "{0} đã ngắt kết nối từ {1}",
+ "UserLockedOutWithName": "User {0} đã bị khóa",
+ "UserDownloadingItemWithValues": "{0} đang tải xuống {1}",
+ "UserDeletedWithName": "Người Dùng {0} đã được xóa",
+ "UserCreatedWithName": "Người Dùng {0} đã được tạo",
+ "User": "Người Dùng",
+ "TvShows": "Chương Trình TV",
+ "System": "Hệ Thống",
+ "SubtitleDownloadFailureFromForItem": "Không thể tải xuống phụ đề từ {0} cho {1}",
+ "StartupEmbyServerIsLoading": "Jellyfin Server đang tải. Vui lòng thử lại trong thời gian ngắn.",
+ "ServerNameNeedsToBeRestarted": "{0} cần được khởi động lại",
+ "ScheduledTaskStartedWithName": "{0} đã bắt đầu",
+ "ScheduledTaskFailedWithName": "{0} đã thất bại",
+ "ProviderValue": "Provider: {0}",
+ "PluginUpdatedWithName": "{0} đã cập nhật",
+ "PluginUninstalledWithName": "{0} đã được gỡ bỏ",
+ "PluginInstalledWithName": "{0} đã được cài đặt",
+ "Plugin": "Plugin",
+ "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng",
+ "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
+ "NotificationOptionUserLockedOut": "Người dùng bị khóa",
+ "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
+ "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server",
+ "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
+ "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
+ "NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
+ "NotificationOptionPluginError": "Thất bại Plugin",
+ "NotificationOptionNewLibraryContent": "Nội dung mới được thêm vào",
+ "NotificationOptionInstallationFailed": "Cài đặt thất bại",
+ "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
+ "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
+ "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu",
+ "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
+ "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
+ "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
+ "NameSeasonUnknown": "Không Rõ Mùa",
+ "NameSeasonNumber": "Mùa {0}",
+ "NameInstallFailed": "{0} cài đặt thất bại",
+ "MusicVideos": "Video Nhạc",
+ "Music": "Nhạc",
+ "MixedContent": "Nội dung hỗn hợp",
+ "MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Phần cấu hình máy chủ {0} đã được cập nhật",
+ "MessageApplicationUpdatedTo": "Jellyfin Server đã được cập nhật lên {0}",
+ "MessageApplicationUpdated": "Jellyfin Server đã được cập nhật",
+ "Latest": "Gần Nhất",
+ "LabelRunningTimeValue": "Thời Gian Chạy: {0}",
+ "LabelIpAddressValue": "Địa Chỉ IP: {0}",
+ "ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
+ "ItemAddedWithName": "{0} được thêm vào thư viện",
+ "Inherit": "Thừa hưởng",
+ "HomeVideos": "Video nhà",
+ "HeaderRecordingGroups": "Nhóm Ghi Video",
+ "HeaderNextUp": "Tiếp Theo",
+ "HeaderFavoriteSongs": "Bài Hát Yêu Thích",
+ "HeaderFavoriteShows": "Chương Trình Yêu Thích",
+ "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",
+ "ChapterNameValue": "Phân Cảnh {0}",
+ "Channels": "Các Kênh",
+ "CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}",
+ "Books": "Sách",
+ "AuthenticationSucceededWithUserName": "{0} xác thực thành công",
+ "Application": "Ứng Dụng",
+ "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index a21cdad95..7b6540c3e 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -1,6 +1,6 @@
{
"Albums": "專輯",
- "AppDeviceValues": "軟體: {0}, 裝置: {1}",
+ "AppDeviceValues": "軟體:{0},裝置:{1}",
"Application": "應用程式",
"Artists": "演出者",
"AuthenticationSucceededWithUserName": "{0} 成功授權",
@@ -11,7 +11,7 @@
"Collections": "合輯",
"DeviceOfflineWithName": "{0} 已經斷線",
"DeviceOnlineWithName": "{0} 已經連線",
- "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
+ "FailedLoginAttemptWithUserName": "來自使用者 {0} 的失敗登入",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
@@ -28,8 +28,8 @@
"HomeVideos": "自製影片",
"ItemAddedWithName": "{0} 已新增至媒體庫",
"ItemRemovedWithName": "{0} 已從媒體庫移除",
- "LabelIpAddressValue": "IP 位置: {0}",
- "LabelRunningTimeValue": "運行時間: {0}",
+ "LabelIpAddressValue": "IP 位址:{0}",
+ "LabelRunningTimeValue": "運行時間:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin Server 已經更新",
"MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
@@ -42,18 +42,18 @@
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
- "NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。",
+ "NewVersionIsAvailable": "新版本的 Jellyfin Server 軟體已經可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
- "NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
+ "NotificationOptionApplicationUpdateInstalled": "軟體更新已安裝",
"NotificationOptionAudioPlayback": "音樂開始播放",
"NotificationOptionAudioPlaybackStopped": "音樂停止播放",
"NotificationOptionCameraImageUploaded": "相機相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已新增新內容",
- "NotificationOptionPluginError": "插件安裝錯誤",
- "NotificationOptionPluginInstalled": "插件已安裝",
- "NotificationOptionPluginUninstalled": "插件已移除",
- "NotificationOptionPluginUpdateInstalled": "插件已更新",
+ "NotificationOptionPluginError": "外掛安裝失敗",
+ "NotificationOptionPluginInstalled": "外掛已安裝",
+ "NotificationOptionPluginUninstalled": "外掛已移除",
+ "NotificationOptionPluginUpdateInstalled": "外掛已更新",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程任務失敗",
"NotificationOptionUserLockedOut": "使用者已鎖定",
@@ -61,14 +61,14 @@
"NotificationOptionVideoPlaybackStopped": "影片停止播放",
"Photos": "相片",
"Playlists": "播放清單",
- "Plugin": "插件",
+ "Plugin": "外掛",
"PluginInstalledWithName": "{0} 已安裝",
"PluginUninstalledWithName": "{0} 已移除",
"PluginUpdatedWithName": "{0} 已更新",
"ProviderValue": "提供商: {0}",
- "ScheduledTaskFailedWithName": "{0} 已失敗",
- "ScheduledTaskStartedWithName": "{0} 已開始",
- "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
+ "ScheduledTaskFailedWithName": "排程任務 {0} 已失敗",
+ "ScheduledTaskStartedWithName": "排程任務 {0} 已開始",
+ "ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。",
@@ -78,10 +78,10 @@
"User": "使用者",
"UserCreatedWithName": "使用者 {0} 已建立",
"UserDeletedWithName": "使用者 {0} 已移除",
- "UserDownloadingItemWithValues": "{0} 正在下載 {1}",
+ "UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}",
"UserLockedOutWithName": "使用者 {0} 已鎖定",
- "UserOfflineFromDevice": "{0} 已從 {1} 斷線",
- "UserOnlineFromDevice": "{0} 已連線,來自 {1}",
+ "UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線",
+ "UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
"UserPolicyUpdatedWithName": "使用者條約已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}",
@@ -95,23 +95,23 @@
"TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
- "TaskUpdatePlugins": "更新插件",
- "TaskRefreshPeople": "重新整理人員",
- "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔案。",
+ "TaskUpdatePlugins": "更新外掛",
+ "TaskRefreshPeople": "刷新用戶",
+ "TaskCleanLogsDescription": "刪除超過 {0} 天的舊紀錄檔。",
"TaskCleanLogs": "清空紀錄資料夾",
- "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。",
- "TaskRefreshLibrary": "掃描媒體庫",
+ "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。",
+ "TaskRefreshLibrary": "重新掃描媒體庫",
"TaskRefreshChapterImages": "擷取章節圖片",
- "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。",
+ "TaskCleanCacheDescription": "刪除系統已不需要的快取。",
"TaskCleanCache": "清除快取資料夾",
"TasksLibraryCategory": "媒體庫",
- "TaskRefreshChannelsDescription": "重新整理網絡頻道資料。",
+ "TaskRefreshChannelsDescription": "重新整理網路頻道資料。",
"TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
"TaskCleanTranscode": "清除轉碼資料夾",
- "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
+ "TaskUpdatePluginsDescription": "為設置自動更新的外掛下載並安裝更新。",
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。",
- "TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。",
- "TasksChannelsCategory": "網絡頻道",
+ "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
+ "TasksChannelsCategory": "網路頻道",
"TasksApplicationCategory": "應用程式",
"TasksMaintenanceCategory": "維修"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 90e2766b8..30aaf3a05 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -413,6 +413,7 @@ namespace Emby.Server.Implementations.Localization
yield return new LocalizationOption("Swedish", "sv");
yield return new LocalizationOption("Swiss German", "gsw");
yield return new LocalizationOption("Turkish", "tr");
+ yield return new LocalizationOption("Tiếng Việt", "vi");
}
}
}
diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs
new file mode 100644
index 000000000..33762791b
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/PluginManifest.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace Emby.Server.Implementations.Plugins
+{
+ /// <summary>
+ /// Defines a Plugin manifest file.
+ /// </summary>
+ public class PluginManifest
+ {
+ /// <summary>
+ /// Gets or sets the category of the plugin.
+ /// </summary>
+ public string Category { get; set; }
+
+ /// <summary>
+ /// Gets or sets the changelog information.
+ /// </summary>
+ public string Changelog { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description of the plugin.
+ /// </summary>
+ public string Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Global Unique Identifier for the plugin.
+ /// </summary>
+ public Guid Guid { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Name of the plugin.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets an overview of the plugin.
+ /// </summary>
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the owner of the plugin.
+ /// </summary>
+ public string Owner { get; set; }
+
+ /// <summary>
+ /// Gets or sets the compatibility version for the plugin.
+ /// </summary>
+ public string TargetAbi { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timestamp of the plugin.
+ /// </summary>
+ public DateTime Timestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Version number of the plugin.
+ /// </summary>
+ public string Version { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
index 4dfadc703..29393ae07 100644
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -257,8 +257,7 @@ namespace Emby.Server.Implementations.Security
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts)
- .ToList();
+ var statements = PrepareAll(db, statementTexts);
using (var statement = statements[0])
{
@@ -282,7 +281,7 @@ namespace Emby.Server.Implementations.Security
ReadTransactionMode);
}
- result.Items = list.ToArray();
+ result.Items = list;
return result;
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index ca8e0e29b..e42d47853 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1037,7 +1037,7 @@ namespace Emby.Server.Implementations.Session
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayMessage.ToString()
+ Name = GeneralCommandType.DisplayMessage
};
generalCommand.Arguments["Header"] = command.Header;
@@ -1268,7 +1268,7 @@ namespace Emby.Server.Implementations.Session
{
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayContent.ToString(),
+ Name = GeneralCommandType.DisplayContent,
Arguments =
{
["ItemId"] = command.ItemId,
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 73fcbcec3..b7a59cee2 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -68,12 +68,6 @@ namespace Emby.Server.Implementations.Udp
{
_logger.LogError(ex, "Error sending response message");
}
-
- var parts = messageText.Split('|');
- if (parts.Length > 1)
- {
- _appHost.EnableLoopback(parts[1]);
- }
}
else
{
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index f121a3493..003cf3c74 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -15,12 +15,14 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
+using MediaBrowser.Common.System;
using MediaBrowser.Controller.Configuration;
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
{
@@ -153,7 +155,12 @@ namespace Emby.Server.Implementations.Updates
var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
{
- result.AddRange(await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true));
+ foreach (var package in await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true))
+ {
+ package.repositoryName = repository.Name;
+ package.repositoryUrl = repository.Url;
+ result.Add(package);
+ }
}
return result;
@@ -183,7 +190,8 @@ namespace Emby.Server.Implementations.Updates
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
- Version minVersion = null)
+ Version minVersion = null,
+ Version specificVersion = null)
{
var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
@@ -197,7 +205,11 @@ namespace Emby.Server.Implementations.Updates
var availableVersions = package.versions
.Where(x => Version.Parse(x.targetAbi) <= appVer);
- if (minVersion != null)
+ if (specificVersion != null)
+ {
+ availableVersions = availableVersions.Where(x => new Version(x.version) == specificVersion);
+ }
+ else if (minVersion != null)
{
availableVersions = availableVersions.Where(x => new Version(x.version) >= minVersion);
}
@@ -227,8 +239,8 @@ namespace Emby.Server.Implementations.Updates
{
foreach (var plugin in _applicationHost.Plugins)
{
- var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version);
- var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version);
+ var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
+ var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
{
yield return version;
@@ -372,9 +384,19 @@ namespace Emby.Server.Implementations.Updates
throw new InvalidDataException("The checksum of the received data doesn't match.");
}
+ // Version folder as they cannot be overwritten in Windows.
+ targetDir += "_" + package.Version;
+
if (Directory.Exists(targetDir))
{
- Directory.Delete(targetDir, true);
+ try
+ {
+ Directory.Delete(targetDir, true);
+ }
+ catch
+ {
+ // Ignore any exceptions.
+ }
}
stream.Position = 0;
@@ -418,15 +440,22 @@ namespace Emby.Server.Implementations.Updates
path = file;
}
- if (isDirectory)
+ try
{
- _logger.LogInformation("Deleting plugin directory {0}", path);
- Directory.Delete(path, true);
+ if (isDirectory)
+ {
+ _logger.LogInformation("Deleting plugin directory {0}", path);
+ Directory.Delete(path, true);
+ }
+ else
+ {
+ _logger.LogInformation("Deleting plugin file {0}", path);
+ _fileSystem.DeleteFile(path);
+ }
}
- else
+ catch
{
- _logger.LogInformation("Deleting plugin file {0}", path);
- _fileSystem.DeleteFile(path);
+ // Ignore file errors.
}
var list = _config.Configuration.UninstalledPlugins.ToList();
diff --git a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
new file mode 100644
index 000000000..3adb700eb
--- /dev/null
+++ b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Attributes
+{
+ /// <summary>
+ /// Produces file attribute of "image/*".
+ /// </summary>
+ public class ProducesAudioFileAttribute : ProducesFileAttribute
+ {
+ private const string ContentType = "audio/*";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class.
+ /// </summary>
+ public ProducesAudioFileAttribute()
+ : base(ContentType)
+ {
+ }
+ }
+}
diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
new file mode 100644
index 000000000..62a576ede
--- /dev/null
+++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Jellyfin.Api.Attributes
+{
+ /// <summary>
+ /// Internal produces image attribute.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method)]
+ public class ProducesFileAttribute : Attribute
+ {
+ private readonly string[] _contentTypes;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class.
+ /// </summary>
+ /// <param name="contentTypes">Content types this endpoint produces.</param>
+ public ProducesFileAttribute(params string[] contentTypes)
+ {
+ _contentTypes = contentTypes;
+ }
+
+ /// <summary>
+ /// Gets the configured content types.
+ /// </summary>
+ /// <returns>the configured content types.</returns>
+ public string[] GetContentTypes() => _contentTypes;
+ }
+}
diff --git a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
new file mode 100644
index 000000000..e15813676
--- /dev/null
+++ b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Attributes
+{
+ /// <summary>
+ /// Produces file attribute of "image/*".
+ /// </summary>
+ public class ProducesImageFileAttribute : ProducesFileAttribute
+ {
+ private const string ContentType = "image/*";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class.
+ /// </summary>
+ public ProducesImageFileAttribute()
+ : base(ContentType)
+ {
+ }
+ }
+}
diff --git a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
new file mode 100644
index 000000000..5d928ab91
--- /dev/null
+++ b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Attributes
+{
+ /// <summary>
+ /// Produces file attribute of "image/*".
+ /// </summary>
+ public class ProducesPlaylistFileAttribute : ProducesFileAttribute
+ {
+ private const string ContentType = "application/x-mpegURL";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class.
+ /// </summary>
+ public ProducesPlaylistFileAttribute()
+ : base(ContentType)
+ {
+ }
+ }
+}
diff --git a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
new file mode 100644
index 000000000..d8b2856dc
--- /dev/null
+++ b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Attributes
+{
+ /// <summary>
+ /// Produces file attribute of "video/*".
+ /// </summary>
+ public class ProducesVideoFileAttribute : ProducesFileAttribute
+ {
+ private const string ContentType = "video/*";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class.
+ /// </summary>
+ public ProducesVideoFileAttribute()
+ : base(ContentType)
+ {
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index aa366f567..d732b6bc6 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -1,6 +1,7 @@
using System.Security.Claims;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
@@ -69,7 +70,7 @@ namespace Jellyfin.Api.Auth
return false;
}
- var ip = RequestHelpers.NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
+ var ip = _httpContextAccessor.HttpContext.GetNormalizedRemoteIp();
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
// User cannot access remotely and user is remote
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs
index 190d4bd07..357f646a2 100644
--- a/Jellyfin.Api/Controllers/AlbumsController.cs
+++ b/Jellyfin.Api/Controllers/AlbumsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -52,7 +53,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Albums/{albumId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
- [FromRoute] string albumId,
+ [FromRoute, Required] string albumId,
[FromQuery] Guid? userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
@@ -84,7 +85,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Artists/{artistId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
- [FromRoute] string artistId,
+ [FromRoute, Required] string artistId,
[FromQuery] Guid? userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 0e28d4c47..e8d6ccdf2 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Keys")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateKey([FromQuery, Required] string? app)
+ public ActionResult CreateKey([FromQuery, Required] string app)
{
_authRepo.Create(new AuthenticationInfo
{
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Keys/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult RevokeKey([FromRoute, Required] string? key)
+ public ActionResult RevokeKey([FromRoute, Required] string key)
{
_sessionManager.RevokeToken(key);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 3f72830cd..d38214116 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -469,7 +470,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the artist.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetArtistByName([FromRoute] string name, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 802cd026e..d4c6e4af9 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
@@ -83,13 +85,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
+ [HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")]
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
- [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
+ [HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")]
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromRoute] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index bdd7dfd96..33a969f85 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -90,7 +91,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Channel features returned.</response>
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
[HttpGet("{channelId}/Features")]
- public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute] string channelId)
+ public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] string channelId)
{
return _channelManager.GetChannelFeatures(channelId);
}
@@ -114,7 +115,7 @@ namespace Jellyfin.Api.Controllers
/// </returns>
[HttpGet("{channelId}/Items")]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
- [FromRoute] Guid channelId,
+ [FromRoute, Required] Guid channelId,
[FromQuery] Guid? folderId,
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index c5910d6e8..2fc697a6a 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
+ public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
{
await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true);
return NoContent();
@@ -103,7 +103,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
+ public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
{
await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 20fb0ec87..e1c9f69f6 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations;
+using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.ConfigurationDtos;
using MediaBrowser.Common.Json;
@@ -73,7 +75,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>Configuration.</returns>
[HttpGet("Configuration/{key}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<object> GetNamedConfiguration([FromRoute] string? key)
+ [ProducesFile(MediaTypeNames.Application.Json)]
+ public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key)
{
return _configurationManager.GetConfiguration(key);
}
@@ -87,7 +90,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Configuration/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key)
+ public async Task<ActionResult> UpdateNamedConfiguration([FromRoute, Required] string key)
{
var configurationType = _configurationManager.GetConfigurationType(key);
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 3f0fc2e91..a859ac114 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net.Mime;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller;
@@ -106,6 +108,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("web/ConfigurationPage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
{
IPlugin? plugin = null;
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 1aed20ade..74380c2ef 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string? id)
+ public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDevice(id);
if (deviceInfo == null)
@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string? id)
+ public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDeviceOptions(id);
if (deviceInfo == null)
@@ -111,7 +111,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions(
- [FromQuery, Required] string? id,
+ [FromQuery, Required] string id,
[FromBody, Required] DeviceOptions deviceOptions)
{
var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
@@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteDevice([FromQuery, Required] string? id)
+ public ActionResult DeleteDevice([FromQuery, Required] string id)
{
var existingDevice = _deviceManager.GetDevice(id);
if (existingDevice == null)
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index c3b67eec3..874467c75 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -43,9 +43,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
- [FromRoute] string? displayPreferencesId,
- [FromQuery] [Required] Guid userId,
- [FromQuery] [Required] string? client)
+ [FromRoute, Required] string displayPreferencesId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery, Required] string client)
{
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
@@ -97,9 +97,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
public ActionResult UpdateDisplayPreferences(
- [FromRoute] string? displayPreferencesId,
+ [FromRoute, Required] string displayPreferencesId,
[FromQuery, Required] Guid userId,
- [FromQuery, Required] string? client,
+ [FromQuery, Required] string client,
[FromBody, Required] DisplayPreferencesDto displayPreferences)
{
HomeSectionType[] defaults =
diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
index 397299a73..052a6aff2 100644
--- a/Jellyfin.Api/Controllers/DlnaController.cs
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Model.Dlna;
@@ -59,7 +60,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Profiles/{profileId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceProfile> GetProfile([FromRoute] string profileId)
+ public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
{
var profile = _dlnaManager.GetProfile(profileId);
if (profile == null)
@@ -80,7 +81,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Profiles/{profileId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteProfile([FromRoute] string profileId)
+ public ActionResult DeleteProfile([FromRoute, Required] string profileId)
{
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
if (existingDeviceProfile == null)
@@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Profiles/{profileId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateProfile([FromRoute] string profileId, [FromBody] DeviceProfile deviceProfile)
+ public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile)
{
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
if (existingDeviceProfile == null)
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 9ebd89819..271ae293b 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Mime;
@@ -43,9 +44,10 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
[HttpGet("{serverId}/description")]
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
- [Produces(MediaTypeNames.Text.Xml)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult GetDescriptionXml([FromRoute] string serverId)
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
+ public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{
var url = GetAbsoluteUri();
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
@@ -62,10 +64,11 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{serverId}/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
- [Produces(MediaTypeNames.Text.Xml)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetContentDirectory([FromRoute] string serverId)
+ public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{
return Ok(_contentDirectory.GetServiceXml());
}
@@ -78,10 +81,11 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
- [Produces(MediaTypeNames.Text.Xml)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
+ public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{
return Ok(_mediaReceiverRegistrar.GetServiceXml());
}
@@ -94,10 +98,11 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
- [Produces(MediaTypeNames.Text.Xml)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetConnectionManager([FromRoute] string serverId)
+ public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{
return Ok(_connectionManager.GetServiceXml());
}
@@ -108,7 +113,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="serverId">Server UUID.</param>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")]
- public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute] string serverId)
+ public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
}
@@ -119,7 +124,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="serverId">Server UUID.</param>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")]
- public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute] string serverId)
+ public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
}
@@ -130,7 +135,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="serverId">Server UUID.</param>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
- public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string serverId)
+ public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
}
@@ -185,7 +190,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>Icon stream.</returns>
[HttpGet("{serverId}/icons/{fileName}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesImageFile]
+ public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
{
return GetIconInternal(fileName);
}
@@ -196,7 +203,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="fileName">The icon filename.</param>
/// <returns>Icon stream.</returns>
[HttpGet("icons/{fileName}")]
- public ActionResult GetIcon([FromRoute] string fileName)
+ [ProducesImageFile]
+ public ActionResult GetIcon([FromRoute, Required] string fileName)
{
return GetIconInternal(fileName);
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 0c884d58d..1153a601e 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
@@ -112,7 +113,6 @@ namespace Jellyfin.Api.Controllers
/// Gets a video hls playlist stream.
/// </summary>
/// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -166,9 +166,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/master.m3u8")]
[HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
public async Task<ActionResult> GetMasterHlsVideoPlaylist(
- [FromRoute] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -177,7 +177,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
- [FromQuery, Required] string? mediaSourceId,
+ [FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
@@ -221,7 +221,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new HlsVideoRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -279,7 +278,6 @@ namespace Jellyfin.Api.Controllers
/// Gets an audio hls playlist stream.
/// </summary>
/// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -333,9 +331,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Audio/{itemId}/master.m3u8")]
[HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
public async Task<ActionResult> GetMasterHlsAudioPlaylist(
- [FromRoute] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -344,7 +342,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
- [FromQuery, Required] string? mediaSourceId,
+ [FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
@@ -388,7 +386,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new HlsAudioRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -446,7 +443,6 @@ namespace Jellyfin.Api.Controllers
/// Gets a video stream using HTTP live streaming.
/// </summary>
/// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -498,9 +494,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/main.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
public async Task<ActionResult> GetVariantHlsVideoPlaylist(
- [FromRoute] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -553,7 +549,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new VideoRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -611,7 +606,6 @@ namespace Jellyfin.Api.Controllers
/// Gets an audio stream using HTTP live streaming.
/// </summary>
/// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -663,9 +657,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/main.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
public async Task<ActionResult> GetVariantHlsAudioPlaylist(
- [FromRoute] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -718,7 +712,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -830,11 +823,12 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> GetHlsVideoSegment(
- [FromRoute] Guid itemId,
- [FromRoute] string playlistId,
- [FromRoute] int segmentId,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] int segmentId,
[FromRoute] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
@@ -999,11 +993,12 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> GetHlsAudioSegment(
- [FromRoute] Guid itemId,
- [FromRoute] string playlistId,
- [FromRoute] int segmentId,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] int segmentId,
[FromRoute] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
@@ -1137,30 +1132,30 @@ namespace Jellyfin.Api.Controllers
var builder = new StringBuilder();
- builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
- builder.AppendLine("#EXT-X-VERSION:3");
- builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
- builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+ builder.AppendLine("#EXTM3U")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+ .AppendLine("#EXT-X-VERSION:3")
+ .Append("#EXT-X-TARGETDURATION:")
+ .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
+ .AppendLine()
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
- var queryString = Request.QueryString;
var index = 0;
-
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
+ var queryString = Request.QueryString;
foreach (var length in segmentLengths)
{
- builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
- builder.AppendLine(
- string.Format(
- CultureInfo.InvariantCulture,
- "hls1/{0}/{1}{2}{3}",
- name,
- index.ToString(CultureInfo.InvariantCulture),
- segmentExtension,
- queryString));
-
- index++;
+ builder.Append("#EXTINF:")
+ .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
+ .AppendLine(", nodesc")
+ .Append("hls1/")
+ .Append(name)
+ .Append('/')
+ .Append(index++)
+ .Append(segmentExtension)
+ .Append(queryString)
+ .AppendLine();
}
builder.AppendLine("#EXT-X-ENDLIST");
@@ -1458,7 +1453,7 @@ namespace Jellyfin.Api.Controllers
var args = "-codec:v:0 " + codec;
- // if (state.EnableMpegtsM2TsMode)
+ // if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 64670f7d8..ce88b0b99 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -69,11 +69,11 @@ namespace Jellyfin.Api.Controllers
/// Validates path.
/// </summary>
/// <param name="validatePathDto">Validate request object.</param>
- /// <response code="200">Path validated.</response>
+ /// <response code="204">Path validated.</response>
/// <response code="404">Path not found.</response>
/// <returns>Validation status.</returns>
[HttpPost("ValidatePath")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
{
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
}
}
- return Ok();
+ return NoContent();
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 55ad71200..de6aa86c9 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
@@ -260,7 +261,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the genre.</returns>
[HttpGet("{genreName}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetGenre([FromRoute] string genreName, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions()
.AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 816252f80..054e586ce 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -1,8 +1,10 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration;
@@ -54,8 +56,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
- public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
+ public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
// TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path);
@@ -74,8 +77,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
- public ActionResult GetHlsPlaylistLegacy([FromRoute] string itemId, [FromRoute] string playlistId)
+ public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = playlistId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
@@ -112,12 +116,13 @@ namespace Jellyfin.Api.Controllers
// [Authenticated]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsVideoSegmentLegacy(
- [FromRoute] string itemId,
- [FromRoute] string playlistId,
- [FromRoute] string segmentId,
- [FromRoute] string segmentContainer)
+ [FromRoute, Required] string itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] string segmentId,
+ [FromRoute, Required] string segmentContainer)
{
var file = segmentId + Path.GetExtension(Request.Path);
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 528590536..980c3273d 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -65,7 +66,8 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<FileStreamResult> GetGeneralImage([FromRoute, Required] string? name, [FromRoute, Required] string? type)
+ [ProducesImageFile]
+ public ActionResult GetGeneralImage([FromRoute, Required] string name, [FromRoute, Required] string type)
{
var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
? "folder"
@@ -110,9 +112,10 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<FileStreamResult> GetRatingImage(
- [FromRoute, Required] string? theme,
- [FromRoute, Required] string? name)
+ [ProducesImageFile]
+ public ActionResult GetRatingImage(
+ [FromRoute, Required] string theme,
+ [FromRoute, Required] string name)
{
return GetImageFile(_applicationPaths.RatingsPath, theme, name);
}
@@ -143,9 +146,10 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<FileStreamResult> GetMediaInfoImage(
- [FromRoute, Required] string? theme,
- [FromRoute, Required] string? name)
+ [ProducesImageFile]
+ public ActionResult GetMediaInfoImage(
+ [FromRoute, Required] string theme,
+ [FromRoute, Required] string name)
{
return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
}
@@ -157,7 +161,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="theme">Theme to search.</param>
/// <param name="name">File name to search for.</param>
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
- private ActionResult<FileStreamResult> GetImageFile(string basePath, string? theme, string? name)
+ private ActionResult GetImageFile(string basePath, string? theme, string? name)
{
var themeFolder = Path.Combine(basePath, theme);
if (Directory.Exists(themeFolder))
@@ -168,7 +172,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
var contentType = MimeTypes.GetMimeType(path);
- return File(System.IO.File.OpenRead(path), contentType);
+ return PhysicalFile(path, contentType);
}
}
@@ -181,7 +185,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
var contentType = MimeTypes.GetMimeType(path);
- return File(System.IO.File.OpenRead(path), contentType);
+ return PhysicalFile(path, contentType);
}
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index a204fe35c..7afec1219 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Configuration;
@@ -90,8 +92,8 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> PostUserImage(
- [FromRoute] Guid userId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
@@ -137,8 +139,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult DeleteUserImage(
- [FromRoute] Guid userId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
@@ -175,8 +177,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
[FromRoute] int? imageIndex = null)
{
var item = _libraryManager.GetItemById(itemId);
@@ -205,8 +207,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
[FromRoute] int? imageIndex = null)
{
var item = _libraryManager.GetItemById(itemId);
@@ -238,9 +240,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateItemImageIndex(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int imageIndex,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
[FromQuery] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
@@ -264,7 +266,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute] Guid itemId)
+ public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -351,11 +353,12 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
@@ -429,24 +432,25 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetItemImage2(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int maxWidth,
+ [FromRoute, Required] int maxHeight,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromRoute] string tag,
+ [FromRoute, Required] string tag,
[FromQuery] bool? cropWhitespace,
- [FromRoute] string format,
+ [FromRoute, Required] string format,
[FromQuery] bool? addPlayedIndicator,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] double percentPlayed,
+ [FromRoute, Required] int unplayedCount,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromRoute, Required] int imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -507,15 +511,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetArtistImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string tag,
+ [FromQuery] string format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
@@ -524,7 +529,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromRoute, Required] int imageIndex)
{
var item = _libraryManager.GetArtist(name);
if (item == null)
@@ -585,15 +590,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetGenreImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string tag,
+ [FromQuery] string format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
@@ -663,15 +669,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetMusicGenreImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string tag,
+ [FromQuery] string format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
@@ -741,15 +748,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetPersonImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string tag,
+ [FromQuery] string format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
@@ -819,15 +827,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetStudioImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] string tag,
+ [FromRoute, Required] string format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
@@ -897,9 +906,10 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetUserImage(
- [FromRoute] Guid userId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
[FromQuery] string? tag,
[FromQuery] string? format,
[FromQuery] int? maxWidth,
@@ -1297,8 +1307,7 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
- var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read);
- return File(stream, imageContentType);
+ return PhysicalFile(imagePath, imageContentType);
}
}
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 73bd30c4d..07fed9764 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Songs/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -101,7 +101,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Albums/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Playlists/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
- [FromRoute, Required] string? name,
+ [FromRoute, Required] string name,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -211,7 +211,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -248,7 +248,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -285,7 +285,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Items/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index afde4a433..ab73aa428 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -7,6 +7,7 @@ using System.Net.Mime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -18,6 +19,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -72,7 +74,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute] Guid itemId)
+ public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -248,6 +250,8 @@ namespace Jellyfin.Api.Controllers
/// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
/// </returns>
[HttpGet("Items/RemoteSearch/Image")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesImageFile]
public async Task<ActionResult> GetRemoteSearchImage(
[FromQuery, Required] string imageUrl,
[FromQuery, Required] string providerName)
@@ -260,8 +264,7 @@ namespace Jellyfin.Api.Controllers
var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
if (System.IO.File.Exists(contentPath))
{
- await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath);
- return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet);
+ return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
}
}
catch (FileNotFoundException)
@@ -274,10 +277,8 @@ namespace Jellyfin.Api.Controllers
}
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
- // Read the pointer file again
- await using var fileStream = System.IO.File.OpenRead(pointerCachePath);
- return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet);
+ var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+ return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
}
/// <summary>
@@ -291,10 +292,11 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
/// The task result contains an <see cref="NoContentResult"/>.
/// </returns>
- [HttpPost("Items/RemoteSearch/Apply/{id}")]
+ [HttpPost("Items/RemoteSearch/Apply/{itemId}")]
[Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ApplySearchCriteria(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromBody, Required] RemoteSearchResult searchResult,
[FromQuery] bool replaceAllImages = true)
{
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 3f5d305c1..49865eb5e 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -53,7 +54,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult Post(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index ec52f4996..0a6ed31ae 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -68,7 +68,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request)
+ public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -141,7 +141,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Items/{itemId}/MetadataEditor")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute] Guid itemId)
+ public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, Required] string? contentType)
+ public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index f9273bad6..652c4689d 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
@@ -529,7 +530,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/Items/Resume")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index a30873e9e..8a872ae13 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -8,6 +8,7 @@ using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -104,7 +105,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult GetFile([FromRoute] Guid itemId)
+ [ProducesFile("video/*", "audio/*")]
+ public ActionResult GetFile([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -112,8 +114,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- using var fileStream = new FileStream(item.Path, FileMode.Open, FileAccess.Read);
- return File(fileStream, MimeTypes.GetMimeType(item.Path));
+ return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path));
}
/// <summary>
@@ -144,7 +145,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ThemeMediaResult> GetThemeSongs(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false)
{
@@ -210,7 +211,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ThemeMediaResult> GetThemeVideos(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false)
{
@@ -275,7 +276,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<AllThemeMediaResult> GetThemeMedia(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false)
{
@@ -438,7 +439,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute] Guid itemId, [FromQuery] Guid? userId)
+ public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
var item = _libraryManager.GetItemById(itemId);
@@ -555,7 +556,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Library/Movies/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedMovies([FromRoute] string? tmdbId, [FromRoute] string? imdbId)
+ public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
@@ -618,7 +619,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.Download)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> GetDownload([FromRoute] Guid itemId)
+ [ProducesFile("video/*", "audio/*")]
+ public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -687,7 +689,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index eace0f911..3557e6304 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -9,6 +10,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -209,7 +211,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Channels/{channelId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<BaseItemDto> GetChannel([FromRoute] Guid channelId, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -406,7 +408,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Recordings/{recordingId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<BaseItemDto> GetRecording([FromRoute] Guid recordingId, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -428,7 +430,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Tuners/{tunerId}/Reset")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult ResetTuner([FromRoute] string tunerId)
+ public ActionResult ResetTuner([FromRoute, Required] string tunerId)
{
AssertUserCanManageLiveTv();
_liveTvManager.ResetTuner(tunerId, CancellationToken.None);
@@ -446,7 +448,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Timers/{timerId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<TimerInfoDto>> GetTimer(string timerId)
+ public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
{
return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
}
@@ -744,7 +746,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<BaseItemDto>> GetProgram(
- [FromRoute] string programId,
+ [FromRoute, Required] string programId,
[FromQuery] Guid? userId)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -765,7 +767,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteRecording([FromRoute] Guid recordingId)
+ public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
{
AssertUserCanManageLiveTv();
@@ -792,7 +794,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Timers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CancelTimer([FromRoute] string timerId)
+ public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
{
AssertUserCanManageLiveTv();
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
@@ -810,7 +812,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> UpdateTimer([FromRoute] string timerId, [FromBody] TimerInfoDto timerInfo)
+ public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
{
AssertUserCanManageLiveTv();
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
@@ -844,7 +846,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute] string timerId)
+ public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
{
var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false);
if (timer == null)
@@ -884,7 +886,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("SeriesTimers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CancelSeriesTimer([FromRoute] string timerId)
+ public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
{
AssertUserCanManageLiveTv();
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
@@ -902,7 +904,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> UpdateSeriesTimer([FromRoute] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
+ public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
{
AssertUserCanManageLiveTv();
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
@@ -934,7 +936,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("This endpoint is obsolete.")]
- public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute] Guid? groupId)
+ public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
{
return NotFound();
}
@@ -1015,9 +1017,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool validateListings = false,
[FromQuery] bool validateLogin = false)
{
- using var sha = SHA1.Create();
if (!string.IsNullOrEmpty(pw))
{
+ using var sha = SHA1.Create();
listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw)));
}
@@ -1068,6 +1070,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile(MediaTypeNames.Application.Json)]
public async Task<ActionResult> GetSchedulesDirectCountries()
{
var client = _httpClientFactory.CreateClient(NamedClient.Default);
@@ -1176,7 +1179,8 @@ namespace Jellyfin.Api.Controllers
[HttpGet("LiveRecordings/{recordingId}/stream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> GetLiveRecordingFile([FromRoute] string recordingId)
+ [ProducesVideoFile]
+ public async Task<ActionResult> GetLiveRecordingFile([FromRoute, Required] string recordingId)
{
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
@@ -1206,7 +1210,8 @@ namespace Jellyfin.Api.Controllers
[HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> GetLiveStreamFile([FromRoute] string streamId, [FromRoute] string container)
+ [ProducesVideoFile]
+ public async Task<ActionResult> GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
{
var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
if (liveStreamInfo == null)
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 1e154a039..4c21999b1 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -4,10 +4,12 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
using Jellyfin.Api.Models.VideoDtos;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@@ -68,7 +70,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
[HttpGet("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
+ public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
{
return await _mediaInfoHelper.GetPlaybackInfo(
itemId,
@@ -100,7 +102,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] long? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
@@ -164,7 +166,7 @@ namespace Jellyfin.Api.Controllers
enableTranscoding,
allowVideoStreamCopy,
allowAudioStreamCopy,
- Request.HttpContext.Connection.RemoteIpAddress.ToString());
+ Request.HttpContext.GetNormalizedRemoteIp());
}
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
@@ -269,7 +271,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("LiveStreams/Close")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CloseLiveStream([FromQuery, Required] string? liveStreamId)
+ public async Task<ActionResult> CloseLiveStream([FromQuery, Required] string liveStreamId)
{
await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
return NoContent();
@@ -286,6 +288,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Produces(MediaTypeNames.Application.Octet)]
+ [ProducesFile(MediaTypeNames.Application.Octet)]
public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
{
const int MaxSize = 10_000_000;
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 0d319137a..570ae8fdc 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
@@ -258,7 +259,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing a <see cref="BaseItemDto"/> with the music genre.</returns>
[HttpGet("{genreName}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetMusicGenre([FromRoute] string genreName, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 3d6a87909..1d9de14d2 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -44,14 +44,15 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Packages/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
- [FromRoute] [Required] string? name,
+ [FromRoute, Required] string name,
[FromQuery] string? assemblyGuid)
{
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
var result = _installationManager.FilterPackages(
- packages,
- name,
- string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault();
+ packages,
+ name,
+ string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
+ .FirstOrDefault();
return result;
}
@@ -76,6 +77,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="name">Package name.</param>
/// <param name="assemblyGuid">GUID of the associated assembly.</param>
/// <param name="version">Optional version. Defaults to latest version.</param>
+ /// <param name="repositoryUrl">Optional. Specify the repository to install from.</param>
/// <response code="204">Package found.</response>
/// <response code="404">Package not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
@@ -84,16 +86,24 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage(
- [FromRoute] [Required] string? name,
+ [FromRoute, Required] string name,
[FromQuery] string? assemblyGuid,
- [FromQuery] string? version)
+ [FromQuery] string? version,
+ [FromQuery] string? repositoryUrl)
{
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ if (!string.IsNullOrEmpty(repositoryUrl))
+ {
+ packages = packages.Where(p => p.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+ }
+
var package = _installationManager.GetCompatibleVersions(
packages,
name,
string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid),
- string.IsNullOrEmpty(version) ? null : Version.Parse(version)).FirstOrDefault();
+ specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
+ .FirstOrDefault();
if (package == null)
{
@@ -115,7 +125,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CancelPackageInstallation(
- [FromRoute] [Required] Guid packageId)
+ [FromRoute, Required] Guid packageId)
{
_installationManager.CancelInstallation(packageId);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index b6ccec666..8bd610dad 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
@@ -262,7 +263,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BaseItemDto> GetPerson([FromRoute] string name, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions()
.AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index f4c6a9253..1e95bd2b3 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToPlaylist(
- [FromRoute] Guid playlistId,
+ [FromRoute, Required] Guid playlistId,
[FromQuery] string? ids,
[FromQuery] Guid? userId)
{
@@ -103,9 +103,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> MoveItem(
- [FromRoute] string? playlistId,
- [FromRoute] string? itemId,
- [FromRoute] int newIndex)
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] string itemId,
+ [FromRoute, Required] int newIndex)
{
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
return NoContent();
@@ -120,7 +120,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds)
+ public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds)
{
await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false);
return NoContent();
@@ -143,15 +143,15 @@ namespace Jellyfin.Api.Controllers
/// <returns>The original playlist items.</returns>
[HttpGet("{playlistId}/Items")]
public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
- [FromRoute] Guid playlistId,
- [FromRoute] Guid userId,
- [FromRoute] int? startIndex,
- [FromRoute] int? limit,
- [FromRoute] string? fields,
- [FromRoute] bool? enableImages,
- [FromRoute] bool? enableUserData,
- [FromRoute] int? imageTypeLimit,
- [FromRoute] string? enableImageTypes)
+ [FromRoute, Required] Guid playlistId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] string? enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist == null)
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 22f2ca5c3..5c15e9a0d 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -71,8 +72,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkPlayedItem(
- [FromRoute] Guid userId,
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] DateTime? datePlayed)
{
var user = _userManager.GetUserById(userId);
@@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -195,8 +196,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStart(
- [FromRoute] Guid userId,
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
@@ -245,8 +246,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackProgress(
- [FromRoute] Guid userId,
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] long? positionTicks,
[FromQuery] int? audioStreamIndex,
@@ -297,8 +298,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStopped(
- [FromRoute] Guid userId,
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] string? nextMediaType,
[FromQuery] long? positionTicks,
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index a82f2621a..0f8ceba29 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UninstallPlugin([FromRoute] Guid pluginId)
+ public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
{
var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
if (plugin == null)
@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute] Guid pluginId)
+ public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdatePluginConfiguration([FromRoute] Guid pluginId)
+ public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
@@ -172,7 +172,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string? name)
+ public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
{
return new MBRegistrationRecord
{
@@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("Paid plugins are not supported")]
[HttpGet("Registrations/{name}")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
- public ActionResult GetRegistration([FromRoute] string? name)
+ public ActionResult GetRegistration([FromRoute, Required] string name)
{
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility.
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 81aefd15c..5f095443b 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -7,6 +7,7 @@ using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -70,7 +71,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] ImageType? type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
@@ -133,7 +134,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] Guid itemId)
+ public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -155,6 +156,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
{
var urlHash = imageUrl.GetMD5();
@@ -192,7 +194,7 @@ namespace Jellyfin.Api.Controllers
}
var contentType = MimeTypes.GetMimeType(contentPath);
- return File(System.IO.File.OpenRead(contentPath), contentType);
+ return PhysicalFile(contentPath, contentType);
}
/// <summary>
@@ -209,7 +211,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DownloadRemoteImage(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery, Required] ImageType type,
[FromQuery] string? imageUrl)
{
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index e672070c0..ab7920895 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{taskId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<TaskInfo> GetTask([FromRoute, Required] string? taskId)
+ public ActionResult<TaskInfo> GetTask([FromRoute, Required] string taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
@@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Running/{taskId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult StartTask([FromRoute] string? taskId)
+ public ActionResult StartTask([FromRoute, Required] string taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Running/{taskId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult StopTask([FromRoute, Required] string? taskId)
+ public ActionResult StopTask([FromRoute, Required] string taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateTask(
- [FromRoute, Required] string? taskId,
+ [FromRoute, Required] string taskId,
[FromBody, Required] TaskTriggerInfo[] triggerInfos)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index e159a9666..62c870cb1 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -81,7 +81,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] Guid? userId,
- [FromQuery, Required] string? searchTerm,
+ [FromQuery, Required] string searchTerm,
[FromQuery] string? includeItemTypes,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? mediaTypes,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index ba8d51598..39bf6e6dc 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA1801
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -125,10 +123,10 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DisplayContent(
- [FromRoute, Required] string? sessionId,
- [FromQuery, Required] string? itemType,
- [FromQuery, Required] string? itemId,
- [FromQuery, Required] string? itemName)
+ [FromRoute, Required] string sessionId,
+ [FromQuery, Required] string itemType,
+ [FromQuery, Required] string itemId,
+ [FromQuery, Required] string itemName)
{
var command = new BrowseRequest
{
@@ -150,23 +148,23 @@ namespace Jellyfin.Api.Controllers
/// Instructs a session to play an item.
/// </summary>
/// <param name="sessionId">The session id.</param>
+ /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
/// <param name="startPositionTicks">The starting position of the first item.</param>
- /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Playing")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult Play(
- [FromRoute, Required] string? sessionId,
- [FromQuery] Guid[] itemIds,
- [FromQuery] long? startPositionTicks,
- [FromQuery] PlayCommand playCommand)
+ [FromRoute, Required] string sessionId,
+ [FromQuery, Required] PlayCommand playCommand,
+ [FromQuery, Required] string itemIds,
+ [FromQuery] long? startPositionTicks)
{
var playRequest = new PlayRequest
{
- ItemIds = itemIds,
+ ItemIds = RequestHelpers.GetGuids(itemIds),
StartPositionTicks = startPositionTicks,
PlayCommand = playCommand
};
@@ -184,20 +182,29 @@ namespace Jellyfin.Api.Controllers
/// Issues a playstate command to a client.
/// </summary>
/// <param name="sessionId">The session id.</param>
- /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param>
+ /// <param name="command">The <see cref="PlaystateCommand"/>.</param>
+ /// <param name="seekPositionTicks">The optional position ticks.</param>
+ /// <param name="controllingUserId">The optional controlling user id.</param>
/// <response code="204">Playstate command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Playing/{command}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendPlaystateCommand(
- [FromRoute, Required] string? sessionId,
- [FromBody] PlaystateRequest playstateRequest)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] PlaystateCommand command,
+ [FromQuery] long? seekPositionTicks,
+ [FromQuery] string? controllingUserId)
{
_sessionManager.SendPlaystateCommand(
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
sessionId,
- playstateRequest,
+ new PlaystateRequest()
+ {
+ Command = command,
+ ControllingUserId = controllingUserId,
+ SeekPositionTicks = seekPositionTicks,
+ },
CancellationToken.None);
return NoContent();
@@ -214,19 +221,13 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendSystemCommand(
- [FromRoute, Required] string? sessionId,
- [FromRoute, Required] string? command)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] GeneralCommandType command)
{
- var name = command;
- if (Enum.TryParse(name, true, out GeneralCommandType commandType))
- {
- name = commandType.ToString();
- }
-
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var generalCommand = new GeneralCommand
{
- Name = name,
+ Name = command,
ControllingUserId = currentSession.UserId
};
@@ -246,8 +247,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendGeneralCommand(
- [FromRoute, Required] string? sessionId,
- [FromRoute, Required] string? command)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] GeneralCommandType command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -273,7 +274,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendFullGeneralCommand(
- [FromRoute, Required] string? sessionId,
+ [FromRoute, Required] string sessionId,
[FromBody, Required] GeneralCommand command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -307,9 +308,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendMessageCommand(
- [FromRoute, Required] string? sessionId,
- [FromQuery, Required] string? text,
- [FromQuery, Required] string? header,
+ [FromRoute, Required] string sessionId,
+ [FromQuery, Required] string text,
+ [FromQuery] string? header,
[FromQuery] long? timeoutMs)
{
var command = new MessageCommand
@@ -335,8 +336,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddUserToSession(
- [FromRoute, Required] string? sessionId,
- [FromRoute] Guid userId)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] Guid userId)
{
_sessionManager.AddAdditionalUser(sessionId, userId);
return NoContent();
@@ -353,8 +354,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveUserFromSession(
- [FromRoute] string? sessionId,
- [FromRoute] Guid userId)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] Guid userId)
{
_sessionManager.RemoveAdditionalUser(sessionId, userId);
return NoContent();
@@ -375,7 +376,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities(
- [FromQuery, Required] string? id,
+ [FromQuery] string? id,
[FromQuery] string? playableMediaTypes,
[FromQuery] string? supportedCommands,
[FromQuery] bool supportsMediaControl = false,
@@ -434,9 +435,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportViewing(
[FromQuery] string? sessionId,
- [FromQuery] string? itemId)
+ [FromQuery, Required] string? itemId)
{
- string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
_sessionManager.ReportNowViewingItem(session, itemId);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 6f2787d93..cdd5f958e 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -259,7 +260,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the studio.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetStudio([FromRoute] string name, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 988acccc3..cc682ed54 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -9,6 +9,7 @@ using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -86,8 +87,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Task> DeleteSubtitle(
- [FromRoute] Guid itemId,
- [FromRoute] int index)
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int index)
{
var item = _libraryManager.GetItemById(itemId);
@@ -112,8 +113,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
- [FromRoute] Guid itemId,
- [FromRoute, Required] string? language,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string language,
[FromQuery] bool? isPerfectMatch)
{
var video = (Video)_libraryManager.GetItemById(itemId);
@@ -132,8 +133,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
- [FromRoute] Guid itemId,
- [FromRoute, Required] string? subtitleId)
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string subtitleId)
{
var video = (Video)_libraryManager.GetItemById(itemId);
@@ -162,7 +163,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
- public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string? id)
+ [ProducesFile("text/*")]
+ public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string id)
{
var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
@@ -185,11 +187,12 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile("text/*")]
public async Task<ActionResult> GetSubtitle(
[FromRoute, Required] Guid itemId,
- [FromRoute, Required] string? mediaSourceId,
+ [FromRoute, Required] string mediaSourceId,
[FromRoute, Required] int index,
- [FromRoute, Required] string? format,
+ [FromRoute, Required] string format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false,
@@ -211,8 +214,7 @@ namespace Jellyfin.Api.Controllers
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
- FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read);
- return File(stream, MimeTypes.GetMimeType(subtitleStream.Path));
+ return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path));
}
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
@@ -251,11 +253,12 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> GetSubtitlePlaylist(
- [FromRoute] Guid itemId,
- [FromRoute] int index,
- [FromRoute] string? mediaSourceId,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int index,
+ [FromRoute, Required] string mediaSourceId,
[FromQuery, Required] int segmentLength)
{
var item = (Video)_libraryManager.GetItemById(itemId);
@@ -278,7 +281,8 @@ namespace Jellyfin.Api.Controllers
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U")
.Append("#EXT-X-TARGETDURATION:")
- .AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
+ .Append(segmentLength)
+ .AppendLine()
.AppendLine("#EXT-X-VERSION:3")
.AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
@@ -293,8 +297,9 @@ namespace Jellyfin.Api.Controllers
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
builder.Append("#EXTINF:")
- .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
- .AppendLine(",");
+ .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
+ .Append(',')
+ .AppendLine();
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 42db6b6a1..d7c81a3ab 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -53,7 +54,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/Suggestions")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromQuery] string? mediaType,
[FromQuery] string? type,
[FromQuery] int? startIndex,
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index bbfd163de..4cb1984a2 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -3,10 +3,13 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -176,8 +179,8 @@ namespace Jellyfin.Api.Controllers
{
return new EndPointInfo
{
- IsLocal = Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress),
- IsInNetwork = _network.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString())
+ IsLocal = HttpContext.IsLocal(),
+ IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())
};
}
@@ -190,14 +193,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Logs/Log")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult GetLogFile([FromQuery, Required] string? name)
+ [ProducesFile(MediaTypeNames.Text.Plain)]
+ public ActionResult GetLogFile([FromQuery, Required] string name)
{
var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
// For older files, assume fully static
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
-
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
return File(stream, "text/plain");
}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index f463ab889..d158f6c34 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
- [FromQuery, Required] Guid? userId,
+ [FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Upcoming")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
- [FromQuery, Required] Guid? userId,
+ [FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -194,8 +194,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
- [FromRoute, Required] string? seriesId,
- [FromQuery, Required] Guid? userId,
+ [FromRoute, Required] string seriesId,
+ [FromQuery] Guid? userId,
[FromQuery] string? fields,
[FromQuery] int? season,
[FromQuery] string? seasonId,
@@ -317,8 +317,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
- [FromRoute, Required] string? seriesId,
- [FromQuery, Required] Guid? userId,
+ [FromRoute, Required] string seriesId,
+ [FromQuery] Guid? userId,
[FromQuery] string? fields,
[FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index b13cf9fa5..df20a92b3 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -91,8 +94,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)]
+ [ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromRoute] string? container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
@@ -157,7 +161,7 @@ namespace Jellyfin.Api.Controllers
true,
true,
true,
- Request.HttpContext.Connection.RemoteIpAddress.ToString());
+ Request.HttpContext.GetNormalizedRemoteIp());
}
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index d67f82219..50bb8bb2a 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -7,6 +7,7 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.UserDtos;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
@@ -108,7 +109,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.IgnoreParentalControl)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<UserDto> GetUserById([FromRoute] Guid userId)
+ public ActionResult<UserDto> GetUserById([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
@@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers
return NotFound("User not found");
}
- var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString());
+ var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp());
return result;
}
@@ -125,14 +126,14 @@ namespace Jellyfin.Api.Controllers
/// Deletes a user.
/// </summary>
/// <param name="userId">The user id.</param>
- /// <response code="200">User deleted.</response>
+ /// <response code="204">User deleted.</response>
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
[HttpDelete("{userId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteUser([FromRoute] Guid userId)
+ public ActionResult DeleteUser([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
_sessionManager.RevokeUserTokens(user.Id, null);
@@ -156,7 +157,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
[FromRoute, Required] Guid userId,
- [FromQuery, Required] string? pw,
+ [FromQuery, Required] string pw,
[FromQuery] string? password)
{
var user = _userManager.GetUserById(userId);
@@ -203,7 +204,7 @@ namespace Jellyfin.Api.Controllers
DeviceName = auth.Device,
Password = request.Pw,
PasswordSha1 = request.Password,
- RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(),
+ RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(),
Username = request.Username
}).ConfigureAwait(false);
@@ -212,7 +213,7 @@ namespace Jellyfin.Api.Controllers
catch (SecurityException e)
{
// rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e);
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
}
}
@@ -246,7 +247,7 @@ namespace Jellyfin.Api.Controllers
catch (SecurityException e)
{
// rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e);
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
}
}
@@ -255,7 +256,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
- /// <response code="200">Password successfully reset.</response>
+ /// <response code="204">Password successfully reset.</response>
/// <response code="403">User is not allowed to update the password.</response>
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
@@ -265,7 +266,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserPassword(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromBody] UpdateUserPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
@@ -290,7 +291,7 @@ namespace Jellyfin.Api.Controllers
user.Username,
request.CurrentPw,
request.CurrentPw,
- HttpContext.Connection.RemoteIpAddress.ToString(),
+ HttpContext.GetNormalizedRemoteIp(),
false).ConfigureAwait(false);
if (success == null)
@@ -313,7 +314,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
- /// <response code="200">Password successfully reset.</response>
+ /// <response code="204">Password successfully reset.</response>
/// <response code="403">User is not allowed to update the password.</response>
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
@@ -323,7 +324,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateUserEasyPassword(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromBody] UpdateUserEasyPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
@@ -365,7 +366,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUser(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromBody] UserDto updateUser)
{
if (updateUser == null)
@@ -409,7 +410,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserPolicy(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromBody] UserPolicy newPolicy)
{
if (newPolicy == null)
@@ -464,7 +465,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserConfiguration(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromBody] UserConfiguration userConfig)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
@@ -496,7 +497,7 @@ namespace Jellyfin.Api.Controllers
await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
}
- var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString());
+ var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp());
return result;
}
@@ -504,17 +505,17 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Initiates the forgot password process for a local user.
/// </summary>
- /// <param name="enteredUsername">The entered username.</param>
+ /// <param name="forgotPasswordRequest">The forgot password request containing the entered username.</param>
/// <response code="200">Password reset process started.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
[HttpPost("ForgotPassword")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string? enteredUsername)
+ public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
{
- var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress)
- || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString());
+ var isLocal = HttpContext.IsLocal()
+ || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp());
- var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false);
+ var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
return result;
}
@@ -559,7 +560,7 @@ namespace Jellyfin.Api.Controllers
if (filterByNetwork)
{
- if (!_networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()))
+ if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()))
{
users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
}
@@ -567,7 +568,7 @@ namespace Jellyfin.Api.Controllers
var result = users
.OrderBy(u => u.Username)
- .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString()));
+ .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp()));
return result;
}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index f55ff6f3d..48262f062 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -70,7 +71,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the d item.</returns>
[HttpGet("Users/{userId}/Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
@@ -93,7 +94,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
[HttpGet("Users/{userId}/Items/Root")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetRootFolder([FromRoute] Guid userId)
+ public ActionResult<BaseItemDto> GetRootFolder([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
var item = _libraryManager.GetUserRootFolder();
@@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/Intros")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
@@ -138,7 +139,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return MarkFavorite(userId, itemId, true);
}
@@ -152,7 +153,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return MarkFavorite(userId, itemId, false);
}
@@ -166,7 +167,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/Items/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return UpdateUserItemRatingInternal(userId, itemId, null);
}
@@ -181,7 +182,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/Items/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool? likes)
+ public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes)
{
return UpdateUserItemRatingInternal(userId, itemId, likes);
}
@@ -195,7 +196,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>The items local trailers.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
@@ -230,7 +231,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the special features.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
@@ -264,7 +265,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/Items/Latest")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId,
[FromQuery] string? fields,
[FromQuery] string? includeItemTypes,
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 6df7cc779..d575bfc3b 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Extensions;
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/Views")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
[FromQuery] string? presetViews,
[FromQuery] bool includeHidden = false)
@@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/GroupingOptions")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute] Guid userId)
+ public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
if (user == null)
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 76188f46d..2afa878f4 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
@@ -161,8 +163,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
[HttpGet("Videos/{itemId}/live.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 83b03f965..4de7aac71 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -115,7 +116,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{itemId}/AdditionalParts")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid? userId)
+ public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -160,9 +161,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns>
[HttpDelete("{itemId}/AlternateSources")]
[Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteAlternateSources([FromRoute] Guid itemId)
+ public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
{
var video = (Video)_libraryManager.GetItemById(itemId);
@@ -202,7 +203,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult> MergeVersions([FromQuery, Required] string? itemIds)
+ public async Task<ActionResult> MergeVersions([FromQuery, Required] string itemIds)
{
var items = RequestHelpers.Split(itemIds, ',', true)
.Select(i => _libraryManager.GetItemById(i))
@@ -325,13 +326,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStream_2")]
+ [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
[HttpGet("{itemId}/stream")]
- [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
+ [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromRoute] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index eb91ac23e..4ecf0407b 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -179,7 +180,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{year}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BaseItemDto> GetYear([FromRoute] int year, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId)
{
var item = _libraryManager.GetYear(year);
if (item == null)
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 6a8829d46..af0519ffa 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -198,12 +199,12 @@ namespace Jellyfin.Api.Helpers
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
- AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.Request.HttpContext.User);
+ AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
}
AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
- if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.Request.HttpContext.Connection.RemoteIpAddress))
+ if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
{
var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
@@ -334,11 +335,10 @@ namespace Jellyfin.Api.Helpers
}
}
- private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
+ private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, string ipAddress)
{
// Within the local network this will likely do more harm than good.
- var ip = RequestHelpers.NormalizeIp(ipAddress).ToString();
- if (_networkManager.IsInLocalNetwork(ip))
+ if (_networkManager.IsInLocalNetwork(ipAddress))
{
return false;
}
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 884bfbe44..6b516977e 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Net.Http;
using System.Threading;
@@ -123,10 +123,9 @@ namespace Jellyfin.Api.Helpers
state.Dispose();
}
- var memoryStream = new MemoryStream();
- await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
- memoryStream.Position = 0;
- return new FileStreamResult(memoryStream, contentType);
+ await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None)
+ .WriteToAsync(httpContext.Response.Body, CancellationToken.None).ConfigureAwait(false);
+ return new FileStreamResult(httpContext.Response.Body, contentType);
}
finally
{
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 3a736d1e8..1207fb513 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -498,7 +499,7 @@ namespace Jellyfin.Api.Helpers
true,
true,
true,
- httpRequest.HttpContext.Connection.RemoteIpAddress.ToString());
+ httpRequest.HttpContext.GetNormalizedRemoteIp());
}
else
{
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index fbaa69270..8dcf08af5 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Querying;
@@ -119,7 +120,7 @@ namespace Jellyfin.Api.Helpers
authorization.Version,
authorization.DeviceId,
authorization.Device,
- request.HttpContext.Connection.RemoteIpAddress.ToString(),
+ request.HttpContext.GetNormalizedRemoteIp(),
user);
if (session == null)
@@ -172,10 +173,5 @@ namespace Jellyfin.Api.Helpers
.Select(i => i!.Value)
.ToArray();
}
-
- internal static IPAddress NormalizeIp(IPAddress ip)
- {
- return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
- }
}
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index b12590080..f4ec29bde 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Helpers
string? containerInternal = Path.GetExtension(state.RequestedUrl);
- if (string.IsNullOrEmpty(streamingRequest.Container))
+ if (!string.IsNullOrEmpty(streamingRequest.Container))
{
containerInternal = streamingRequest.Container;
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 67e450372..64d1227f7 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -504,6 +504,11 @@ namespace Jellyfin.Api.Helpers
}
}
+ if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
+ {
+ throw new ArgumentException("FFMPEG path not set.");
+ }
+
var process = new Process
{
StartInfo = new ProcessStartInfo
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index ca0542b03..6a00db4b1 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -14,11 +14,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.7" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
- <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
+ <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
new file mode 100644
index 000000000..b31c6539c
--- /dev/null
+++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.UserDtos
+{
+ /// <summary>
+ /// Forgot Password request body DTO.
+ /// </summary>
+ public class ForgotPasswordDto
+ {
+ /// <summary>
+ /// Gets or sets the entered username to have its password reset.
+ /// </summary>
+ [Required]
+ public string? EnteredUsername { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 95343f91b..6bb0d8ce2 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.7" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.7" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.8" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.8" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index c71c76f08..c11ac5fb3 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -18,10 +18,10 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="BlurHashSharp" Version="1.1.0" />
- <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
- <PackageReference Include="SkiaSharp" Version="2.80.1" />
- <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
+ <PackageReference Include="BlurHashSharp" Version="1.1.1" />
+ <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.1" />
+ <PackageReference Include="SkiaSharp" Version="2.80.2" />
+ <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 30ed3e6af..4e79dd8d6 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -24,11 +24,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 08e4db388..45e71f16e 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -1,6 +1,5 @@
#pragma warning disable CS1591
-using System;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Interfaces;
@@ -9,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations
{
/// <inheritdoc/>
- public partial class JellyfinDb : DbContext
+ public class JellyfinDb : DbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDb"/> class.
@@ -138,47 +137,20 @@ namespace Jellyfin.Server.Implementations
return base.SaveChanges();
}
- /// <inheritdoc/>
- public override void Dispose()
- {
- foreach (var entry in ChangeTracker.Entries())
- {
- entry.State = EntityState.Detached;
- }
-
- GC.SuppressFinalize(this);
- base.Dispose();
- }
-
- /// <inheritdoc />
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- CustomInit(optionsBuilder);
- }
-
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
- OnModelCreatingImpl(modelBuilder);
modelBuilder.HasDefaultSchema("jellyfin");
- /*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind);
-
- modelBuilder.Entity<Genre>().HasIndex(t => t.Name)
- .IsUnique();
+ modelBuilder.Entity<DisplayPreferences>()
+ .HasIndex(entity => entity.UserId)
+ .IsUnique(false);
- modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId)
- .IsUnique();*/
-
- OnModelCreatedImpl(modelBuilder);
+ modelBuilder.Entity<DisplayPreferences>()
+ .HasIndex(entity => new { entity.UserId, entity.Client })
+ .IsUnique();
}
-
- partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
-
- partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
-
- partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
}
}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
new file mode 100644
index 000000000..2234f9d5f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs
@@ -0,0 +1,461 @@
+#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("20200905220533_FixDisplayPreferencesIndex")]
+ partial class FixDisplayPreferencesIndex
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "3.1.7");
+
+ 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?>("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/20200905220533_FixDisplayPreferencesIndex.cs b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.cs
new file mode 100644
index 000000000..33c5bb4ca
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.cs
@@ -0,0 +1,51 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class FixDisplayPreferencesIndex : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_DisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "DisplayPreferences");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "DisplayPreferences",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DisplayPreferences_UserId_Client",
+ schema: "jellyfin",
+ table: "DisplayPreferences",
+ columns: new[] { "UserId", "Client" },
+ unique: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_DisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "DisplayPreferences");
+
+ migrationBuilder.DropIndex(
+ name: "IX_DisplayPreferences_UserId_Client",
+ schema: "jellyfin",
+ table: "DisplayPreferences");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "DisplayPreferences",
+ column: "UserId",
+ unique: true);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index a6e6a2324..ccfcf96b1 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.6");
+ .HasAnnotation("ProductVersion", "3.1.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -136,7 +136,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
- b.HasIndex("UserId")
+ b.HasIndex("UserId");
+
+ b.HasIndex("UserId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
diff --git a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
new file mode 100644
index 000000000..0d04b6bb1
--- /dev/null
+++ b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Cors.Infrastructure;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Configuration
+{
+ /// <summary>
+ /// Cors policy provider.
+ /// </summary>
+ public class CorsPolicyProvider : ICorsPolicyProvider
+ {
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CorsPolicyProvider"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public CorsPolicyProvider(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc />
+ public Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName)
+ {
+ var corsHosts = _serverConfigurationManager.Configuration.CorsHosts;
+ var builder = new CorsPolicyBuilder()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+
+ // No hosts configured or only default configured.
+ if (corsHosts.Length == 0
+ || (corsHosts.Length == 1
+ && string.Equals(corsHosts[0], CorsConstants.AnyOrigin, StringComparison.Ordinal)))
+ {
+ builder.AllowAnyOrigin();
+ }
+ else
+ {
+ builder.WithOrigins(corsHosts)
+ .AllowCredentials();
+ }
+
+ return Task.FromResult(builder.Build());
+ }
+ }
+}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 517d77412..5bcf6d5f0 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net;
using System.Reflection;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
@@ -15,17 +16,20 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
+using Jellyfin.Server.Configuration;
+using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters;
-using Jellyfin.Server.Models;
using MediaBrowser.Common.Json;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
+using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
namespace Jellyfin.Server.Extensions
{
@@ -134,17 +138,23 @@ namespace Jellyfin.Server.Extensions
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
+ /// <param name="knownProxies">A list of all known proxies to trust for X-Forwarded-For.</param>
/// <returns>The MVC builder.</returns>
- public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies)
+ public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies, IReadOnlyList<string> knownProxies)
{
IMvcBuilder mvcBuilder = serviceCollection
- .AddCors(options =>
- {
- options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
- })
+ .AddCors()
+ .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+ for (var i = 0; i < knownProxies.Count; i++)
+ {
+ if (IPAddress.TryParse(knownProxies[i], out var address))
+ {
+ options.KnownProxies.Add(address);
+ }
+ }
})
.AddMvc(opts =>
{
@@ -248,6 +258,8 @@ namespace Jellyfin.Server.Extensions
// TODO - remove when all types are supported in System.Text.Json
c.AddSwaggerTypeMappings();
+
+ c.OperationFilter<FileResponseFilter>();
});
}
diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs
new file mode 100644
index 000000000..8ea35c281
--- /dev/null
+++ b/Jellyfin.Server/Filters/FileResponseFilter.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Attributes;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Jellyfin.Server.Filters
+{
+ /// <inheritdoc />
+ public class FileResponseFilter : IOperationFilter
+ {
+ private const string SuccessCode = "200";
+ private static readonly OpenApiMediaType _openApiMediaType = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Type = "file"
+ }
+ };
+
+ /// <inheritdoc />
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata)
+ {
+ if (attribute is ProducesFileAttribute producesFileAttribute)
+ {
+ // Get operation response values.
+ var (_, value) = operation.Responses
+ .FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
+
+ // Operation doesn't have a response.
+ if (value == null)
+ {
+ continue;
+ }
+
+ // Clear existing responses.
+ value.Content.Clear();
+
+ // Add all content-types as file.
+ foreach (var contentType in producesFileAttribute.GetContentTypes())
+ {
+ value.Content.Add(contentType, _openApiMediaType);
+ }
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index c3bec1c71..761a92f6d 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
@@ -13,6 +13,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
</PropertyGroup>
<ItemGroup>
@@ -23,10 +24,6 @@
<EmbeddedResource Include="Resources/Configuration/*" />
</ItemGroup>
- <ItemGroup>
- <FrameworkReference Include="Microsoft.AspNetCore.App" />
- </ItemGroup>
-
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
@@ -41,10 +38,10 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.7" />
+ <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="Serilog.AspNetCore" Version="3.4.0" />
@@ -53,8 +50,8 @@
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
- <PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.3" />
- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.3" />
+ <PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.1" />
+ <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
</ItemGroup>
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index ae3a3a1c5..9316737bd 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -44,7 +44,11 @@ namespace Jellyfin.Server.Middleware
var localPath = httpContext.Request.Path.ToString();
var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl;
- if (!localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
+ || string.IsNullOrEmpty(localPath)
+ || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
{
// Always redirect back to the default path if the base prefix is invalid or missing
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index 63effafc1..fb1ee3b2b 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -125,6 +125,7 @@ namespace Jellyfin.Server.Middleware
switch (ex)
{
case ArgumentException _: return StatusCodes.Status400BadRequest;
+ case AuthenticationException _:
case SecurityException _: return StatusCodes.Status401Unauthorized;
case DirectoryNotFoundException _:
case FileNotFoundException _:
diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
index 59b5fb1ed..4bda8f273 100644
--- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
+++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
@@ -32,13 +32,13 @@ namespace Jellyfin.Server.Middleware
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
{
- if (httpContext.Request.IsLocal())
+ if (httpContext.IsLocal())
{
await _next(httpContext).ConfigureAwait(false);
return;
}
- var remoteIp = httpContext.Request.RemoteIp();
+ var remoteIp = httpContext.GetNormalizedRemoteIp();
if (serverConfigurationManager.Configuration.EnableRemoteAccess)
{
diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
index 3122d92cb..74874da1b 100644
--- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
@@ -69,7 +70,7 @@ namespace Jellyfin.Server.Middleware
_logger.LogWarning(
"Slow HTTP Response from {url} to {remoteIp} in {elapsed:g} with Status Code {statusCode}",
context.Request.GetDisplayUrl(),
- context.Connection.RemoteIpAddress,
+ context.GetNormalizedRemoteIp(),
watch.Elapsed,
context.Response.StatusCode);
}
diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
index ea81c03a2..2ec063392 100644
--- a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
@@ -1,3 +1,4 @@
+using System;
using System.Net.Mime;
using System.Threading.Tasks;
using MediaBrowser.Controller;
@@ -34,7 +35,8 @@ namespace Jellyfin.Server.Middleware
IServerApplicationHost serverApplicationHost,
ILocalizationManager localizationManager)
{
- if (serverApplicationHost.CoreStartupHasCompleted)
+ if (serverApplicationHost.CoreStartupHasCompleted
+ || httpContext.Request.Path.Equals("/system/ping", StringComparison.OrdinalIgnoreCase))
{
await _next(httpContext).ConfigureAwait(false);
return;
diff --git a/Jellyfin.Server/Models/ServerCorsPolicy.cs b/Jellyfin.Server/Models/ServerCorsPolicy.cs
deleted file mode 100644
index ae010c042..000000000
--- a/Jellyfin.Server/Models/ServerCorsPolicy.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Microsoft.AspNetCore.Cors.Infrastructure;
-
-namespace Jellyfin.Server.Models
-{
- /// <summary>
- /// Server Cors Policy.
- /// </summary>
- public static class ServerCorsPolicy
- {
- /// <summary>
- /// Default policy name.
- /// </summary>
- public const string DefaultPolicyName = "DefaultCorsPolicy";
-
- /// <summary>
- /// Default Policy. Allow Everything.
- /// </summary>
- public static readonly CorsPolicy DefaultPolicy = new CorsPolicy
- {
- // Allow any origin
- Origins = { "*" },
-
- // Allow any method
- Methods = { "*" },
-
- // Allow any header
- Headers = { "*" }
- };
- }
-} \ No newline at end of file
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 45959aec2..c933d679f 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -527,6 +527,13 @@ namespace Jellyfin.Server
}
}
+ // Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
+ dataDir = Path.GetFullPath(dataDir);
+ logDir = Path.GetFullPath(logDir);
+ configDir = Path.GetFullPath(configDir);
+ cacheDir = Path.GetFullPath(cacheDir);
+ webDir = Path.GetFullPath(webDir);
+
// Ensure the main folders exist before we continue
try
{
diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json
index b6e2bcf97..20d432afc 100644
--- a/Jellyfin.Server/Properties/launchSettings.json
+++ b/Jellyfin.Server/Properties/launchSettings.json
@@ -2,6 +2,8 @@
"profiles": {
"Jellyfin.Server": {
"commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:8096",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -12,6 +14,16 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"commandLineArgs": "--nowebclient"
+ },
+ "Jellyfin.Server (API Docs)": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "api-docs/swagger",
+ "applicationUrl": "http://localhost:8096",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "commandLineArgs": "--nowebclient"
}
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 597323b86..2f4620aa6 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -5,7 +5,6 @@ using Jellyfin.Api.TypeConverters;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Middleware;
-using Jellyfin.Server.Models;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -53,7 +52,7 @@ namespace Jellyfin.Server
{
options.HttpsPort = _serverApplicationHost.HttpsPort;
});
- services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies());
+ services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.Configuration.KnownProxies);
services.AddJellyfinApiSwagger();
@@ -94,11 +93,7 @@ namespace Jellyfin.Server
IWebHostEnvironment env,
IConfiguration appConfig)
{
- // Only add base url redirection if a base url is set.
- if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.BaseUrl))
- {
- app.UseBaseUrlRedirection();
- }
+ app.UseBaseUrlRedirection();
// Wrap rest of configuration so everything only listens on BaseUrl.
app.Map(_serverConfigurationManager.Configuration.BaseUrl, mainApp =>
@@ -108,6 +103,7 @@ namespace Jellyfin.Server
mainApp.UseDeveloperExceptionPage();
}
+ mainApp.UseForwardedHeaders();
mainApp.UseMiddleware<ExceptionMiddleware>();
mainApp.UseMiddleware<ResponseTimeMiddleware>();
@@ -116,7 +112,7 @@ namespace Jellyfin.Server
mainApp.UseResponseCompression();
- mainApp.UseCors(ServerCorsPolicy.DefaultPolicyName);
+ mainApp.UseCors();
if (_serverConfigurationManager.Configuration.RequireHttps
&& _serverApplicationHost.ListenWithHttps)
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index 41a1430d2..b63434092 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -64,10 +64,6 @@ namespace Jellyfin.Server
public bool IsService { get; set; }
/// <inheritdoc />
- [Option("noautorunwebapp", Required = false, HelpText = "Run headless if startup wizard is complete.")]
- public bool NoAutoRunWebApp { get; set; }
-
- /// <inheritdoc />
[Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")]
public string? PackageName { get; set; }
diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
index e0cf3f9ac..19fa95480 100644
--- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
+++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
@@ -1,5 +1,4 @@
using System.Net;
-using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Common.Extensions
@@ -10,54 +9,33 @@ namespace MediaBrowser.Common.Extensions
public static class HttpContextExtensions
{
/// <summary>
- /// Checks the origin of the HTTP request.
+ /// Checks the origin of the HTTP context.
/// </summary>
- /// <param name="request">The incoming HTTP request.</param>
+ /// <param name="context">The incoming HTTP context.</param>
/// <returns><c>true</c> if the request is coming from LAN, <c>false</c> otherwise.</returns>
- public static bool IsLocal(this HttpRequest request)
+ public static bool IsLocal(this HttpContext context)
{
- return (request.HttpContext.Connection.LocalIpAddress == null
- && request.HttpContext.Connection.RemoteIpAddress == null)
- || request.HttpContext.Connection.LocalIpAddress.Equals(request.HttpContext.Connection.RemoteIpAddress);
+ return (context.Connection.LocalIpAddress == null
+ && context.Connection.RemoteIpAddress == null)
+ || context.Connection.LocalIpAddress.Equals(context.Connection.RemoteIpAddress);
}
/// <summary>
- /// Extracts the remote IP address of the caller of the HTTP request.
+ /// Extracts the remote IP address of the caller of the HTTP context.
/// </summary>
- /// <param name="request">The HTTP request.</param>
+ /// <param name="context">The HTTP context.</param>
/// <returns>The remote caller IP address.</returns>
- public static string RemoteIp(this HttpRequest request)
+ public static string GetNormalizedRemoteIp(this HttpContext context)
{
- var cachedRemoteIp = request.HttpContext.Items["RemoteIp"]?.ToString();
- if (!string.IsNullOrEmpty(cachedRemoteIp))
- {
- return cachedRemoteIp;
- }
-
- IPAddress ip;
-
- // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
- // (if the server is behind a reverse proxy for example)
- if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XForwardedFor].ToString(), out ip))
- {
- if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XRealIP].ToString(), out ip))
- {
- ip = request.HttpContext.Connection.RemoteIpAddress;
-
- // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
- ip ??= IPAddress.Loopback;
- }
- }
+ // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
+ var ip = context.Connection.RemoteIpAddress ?? IPAddress.Loopback;
if (ip.IsIPv4MappedToIPv6)
{
ip = ip.MapToIPv4();
}
- var normalizedIp = ip.ToString();
-
- request.HttpContext.Items["RemoteIp"] = normalizedIp;
- return normalizedIp;
+ return ip.ToString();
}
}
}
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 70dcc2397..322740cca 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.7" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.8" />
<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/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
index 4b4030bc2..169aca2ca 100644
--- a/MediaBrowser.Common/Updates/IInstallationManager.cs
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -73,12 +73,14 @@ namespace MediaBrowser.Common.Updates
/// <param name="name">The name.</param>
/// <param name="guid">The guid of the plugin.</param>
/// <param name="minVersion">The minimum required version of the plugin.</param>
+ /// <param name="specificVersion">The specific version of the plugin to install.</param>
/// <returns>All compatible versions ordered from newest to oldest.</returns>
IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
- Version minVersion = null);
+ Version minVersion = null,
+ Version specificVersion = null);
/// <summary>
/// Returns the available plugin updates.
diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
index 1074ce435..137f5d095 100644
--- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
+++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
@@ -42,6 +42,7 @@ namespace MediaBrowser.Controller.Channels
/// Indicates if a sort ascending/descending toggle is supported or not.
/// </summary>
public bool SupportsSortOrderToggle { get; set; }
+
/// <summary>
/// Gets or sets the automatic refresh levels.
/// </summary>
@@ -53,6 +54,7 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <value>The daily download limit.</value>
public int? DailyDownloadLimit { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether [supports downloading].
/// </summary>
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index 2c6dea02c..8220464b3 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -90,7 +90,6 @@ namespace MediaBrowser.Controller.Entities.Audio
var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty;
-
if (ParentIndexNumber.HasValue)
{
songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 68126bd8a..2fc7d45c9 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -197,6 +197,7 @@ namespace MediaBrowser.Controller.Entities
public virtual bool SupportsRemoteImageDownloading => true;
private string _name;
+
/// <summary>
/// Gets or sets the name.
/// </summary>
@@ -661,6 +662,7 @@ namespace MediaBrowser.Controller.Entities
}
private string _forcedSortName;
+
/// <summary>
/// Gets or sets the name of the forced sort.
/// </summary>
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 11542c1ca..901ea875b 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1386,7 +1386,6 @@ namespace MediaBrowser.Controller.Entities
}
}
-
/// <summary>
/// Gets the linked children.
/// </summary>
diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
index a7b60d168..0f612262a 100644
--- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs
+++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
@@ -21,7 +21,5 @@ namespace MediaBrowser.Controller.Entities
List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
List<MediaStream> GetMediaStreams();
-
-
}
}
diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs
index 1485d4c79..2fc66176f 100644
--- a/MediaBrowser.Controller/Entities/Photo.cs
+++ b/MediaBrowser.Controller/Entities/Photo.cs
@@ -16,7 +16,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override Folder LatestItemsIndexContainer => AlbumEntity;
-
[JsonIgnore]
public PhotoAlbum AlbumEntity
{
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 72c696c1a..75a746bfb 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -450,7 +450,6 @@ namespace MediaBrowser.Controller.Entities.TV
});
}
-
protected override bool GetBlockUnratedValue(User user)
{
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series.ToString());
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index b384b27d1..068a76769 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -258,7 +258,6 @@ namespace MediaBrowser.Controller.Entities
IncludeItemTypes = new[] { typeof(Movie).Name },
Recursive = true,
EnableTotalRecordCount = false
-
}).Items
.SelectMany(i => i.Genres)
.DistinctNames()
diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs
index 3cc1f328a..182c8ef65 100644
--- a/MediaBrowser.Controller/Extensions/StringExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -15,11 +16,6 @@ namespace MediaBrowser.Controller.Extensions
{
public static string RemoveDiacritics(this string text)
{
- if (text == null)
- {
- throw new ArgumentNullException(nameof(text));
- }
-
var chars = Normalize(text, NormalizationForm.FormD)
.Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark);
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
index 9bc4cac39..3db60ae0b 100644
--- a/MediaBrowser.Controller/IO/FileData.cs
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -111,5 +111,4 @@ namespace MediaBrowser.Controller.IO
return returnResult;
}
}
-
}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 9f4c00e1c..cfad17fb7 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -114,8 +114,6 @@ namespace MediaBrowser.Controller
/// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception>
void LaunchUrl(string url);
- void EnableLoopback(string appName);
-
IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
string ExpandVirtualPath(string path);
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index d2f937d4f..332730bcc 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -77,6 +77,7 @@ namespace MediaBrowser.Controller.Library
MusicArtist GetArtist(string name);
MusicArtist GetArtist(string name, DtoOptions options);
+
/// <summary>
/// Gets a Studio.
/// </summary>
@@ -200,7 +201,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Creates the items.
/// </summary>
- void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken);
+ void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken);
/// <summary>
/// Updates the item.
@@ -234,6 +235,7 @@ namespace MediaBrowser.Controller.Library
/// Occurs when [item updated].
/// </summary>
event EventHandler<ItemChangeEventArgs> ItemUpdated;
+
/// <summary>
/// Occurs when [item removed].
/// </summary>
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 9e7b1e608..22bf9488f 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -28,12 +28,14 @@ namespace MediaBrowser.Controller.Library
/// <param name="itemId">The item identifier.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
List<MediaStream> GetMediaStreams(Guid itemId);
+
/// <summary>
/// Gets the media streams.
/// </summary>
/// <param name="mediaSourceId">The media source identifier.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
List<MediaStream> GetMediaStreams(string mediaSourceId);
+
/// <summary>
/// Gets the media streams.
/// </summary>
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 6a0dbeba2..12a311dc3 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -156,6 +156,7 @@ namespace MediaBrowser.Controller.Library
}
// REVIEW: @bond
+
/// <summary>
/// Gets the physical locations.
/// </summary>
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
index 21f33ad19..1c90bb4e0 100644
--- a/MediaBrowser.Controller/Library/NameExtensions.cs
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -9,7 +10,7 @@ namespace MediaBrowser.Controller.Library
{
public static class NameExtensions
{
- private static string RemoveDiacritics(string name)
+ private static string RemoveDiacritics(string? name)
{
if (name == null)
{
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index 55c330931..6c365caa4 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -231,6 +231,7 @@ namespace MediaBrowser.Controller.LiveTv
/// Saves the tuner host.
/// </summary>
Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
+
/// <summary>
/// Saves the listing provider.
/// </summary>
diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
index ff92bf856..abca8f239 100644
--- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs
+++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
@@ -56,7 +56,6 @@ namespace MediaBrowser.Controller.LiveTv
Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken);
Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken);
-
}
public interface IConfigurableTunerHost
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
index 02178297b..b62974904 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
@@ -42,6 +42,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The tuners.</value>
public List<LiveTvTunerInfo> Tuners { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether this instance is visible.
/// </summary>
diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
index bdcffd5ca..f9f559ee9 100644
--- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
@@ -35,6 +35,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The overview.</value>
public string Overview { get; set; }
+
/// <summary>
/// Gets or sets the short overview.
/// </summary>
@@ -169,31 +170,37 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The production year.</value>
public int? ProductionYear { get; set; }
+
/// <summary>
/// Gets or sets the home page URL.
/// </summary>
/// <value>The home page URL.</value>
public string HomePageUrl { get; set; }
+
/// <summary>
/// Gets or sets the series identifier.
/// </summary>
/// <value>The series identifier.</value>
public string SeriesId { get; set; }
+
/// <summary>
/// Gets or sets the show identifier.
/// </summary>
/// <value>The show identifier.</value>
public string ShowId { get; set; }
+
/// <summary>
/// Gets or sets the season number.
/// </summary>
/// <value>The season number.</value>
public int? SeasonNumber { get; set; }
+
/// <summary>
/// Gets or sets the episode number.
/// </summary>
/// <value>The episode number.</value>
public int? EpisodeNumber { get; set; }
+
/// <summary>
/// Gets or sets the etag.
/// </summary>
diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
index 303882b7e..69190694f 100644
--- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
@@ -187,6 +187,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
public bool? HasImage { get; set; }
+
/// <summary>
/// Gets or sets the show identifier.
/// </summary>
diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
index bcef4666d..aa5170617 100644
--- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
@@ -113,6 +113,7 @@ namespace MediaBrowser.Controller.LiveTv
// Program properties
public int? SeasonNumber { get; set; }
+
/// <summary>
/// Gets or sets the episode number.
/// </summary>
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 9854ec520..654470406 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -14,8 +14,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.7" />
+ <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"/>
</ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 2c30ca458..c5529ad5b 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -109,7 +109,6 @@ namespace MediaBrowser.Controller.MediaEncoding
}
return _mediaEncoder.SupportsHwaccel("vaapi");
-
}
/// <summary>
@@ -508,6 +507,7 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append("-hwaccel qsv ");
}
}
+
// While using SW decoder
else
{
@@ -1441,7 +1441,6 @@ namespace MediaBrowser.Controller.MediaEncoding
var codec = outputAudioCodec ?? string.Empty;
-
int? transcoderChannelLimit;
if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
{
@@ -2511,7 +2510,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return inputModifier;
}
-
public void AttachMediaSourceInfo(
EncodingJobInfo state,
MediaSourceInfo mediaSource,
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 68bc502a0..c7ec878d2 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -697,10 +697,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// The progressive.
/// </summary>
Progressive,
+
/// <summary>
/// The HLS.
/// </summary>
Hls,
+
/// <summary>
/// The dash.
/// </summary>
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index ebc37bd1f..45c6805f0 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -100,6 +100,7 @@ namespace MediaBrowser.Controller.Persistence
/// <param name="query">The query.</param>
/// <returns>IEnumerable&lt;Guid&gt;.</returns>
QueryResult<Guid> GetItemIds(InternalItemsQuery query);
+
/// <summary>
/// Gets the items.
/// </summary>
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index f77455485..16fd1d42b 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.IO;
@@ -11,11 +12,11 @@ namespace MediaBrowser.Controller.Providers
{
private readonly IFileSystem _fileSystem;
- private readonly Dictionary<string, FileSystemMetadata[]> _cache = new Dictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new ConcurrentDictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
- private readonly Dictionary<string, FileSystemMetadata> _fileCache = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new ConcurrentDictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
- private readonly Dictionary<string, List<string>> _filePathCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new ConcurrentDictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
public DirectoryService(IFileSystem fileSystem)
{
@@ -24,14 +25,7 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
- if (!_cache.TryGetValue(path, out FileSystemMetadata[] entries))
- {
- entries = _fileSystem.GetFileSystemEntries(path).ToArray();
-
- _cache[path] = entries;
- }
-
- return entries;
+ return _cache.GetOrAdd(path, p => _fileSystem.GetFileSystemEntries(p).ToArray());
}
public List<FileSystemMetadata> GetFiles(string path)
@@ -51,21 +45,19 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata GetFile(string path)
{
- if (!_fileCache.TryGetValue(path, out FileSystemMetadata file))
+ var result = _fileCache.GetOrAdd(path, p =>
{
- file = _fileSystem.GetFileInfo(path);
+ var file = _fileSystem.GetFileInfo(p);
+ return file != null && file.Exists ? file : null;
+ });
- if (file != null && file.Exists)
- {
- _fileCache[path] = file;
- }
- else
- {
- return null;
- }
+ if (result == null)
+ {
+ // lets not store null results in the cache
+ _fileCache.TryRemove(path, out _);
}
- return file;
+ return result;
}
public IReadOnlyList<string> GetFilePaths(string path)
@@ -73,14 +65,12 @@ namespace MediaBrowser.Controller.Providers
public IReadOnlyList<string> GetFilePaths(string path, bool clearCache)
{
- if (clearCache || !_filePathCache.TryGetValue(path, out List<string> result))
+ if (clearCache)
{
- result = _fileSystem.GetFilePaths(path).ToList();
-
- _filePathCache[path] = result;
+ _filePathCache.TryRemove(path, out _);
}
- return result;
+ return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(p).ToList());
}
}
}
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index b99c46843..eb7fb793a 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -19,6 +19,7 @@ namespace MediaBrowser.Controller.Resolvers
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
BaseItem ResolvePath(ItemResolveArgs args);
+
/// <summary>
/// Gets the priority.
/// </summary>
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 054fd33d9..55e44c19d 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -22,7 +22,6 @@ namespace MediaBrowser.Controller.Session
private readonly ISessionManager _sessionManager;
private readonly ILogger _logger;
-
private readonly object _progressLock = new object();
private Timer _progressTimer;
private PlaybackProgressInfo _lastProgressInfo;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index c8bf5557b..3287f9814 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -212,7 +212,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (match.Success)
{
- return new Version(match.Groups[1].Value);
+ if (Version.TryParse(match.Groups[1].Value, out var result))
+ {
+ return result;
+ }
}
var versionMap = GetFFmpegLibraryVersions(output);
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 97748bd0c..8b78ad842 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -83,8 +83,6 @@ namespace MediaBrowser.Model.Configuration
/// </summary>
public bool QuickConnectAvailable { get; set; }
- public bool AutoRunWebApp { get; set; }
-
public bool EnableRemoteAccess { get; set; }
/// <summary>
@@ -264,6 +262,16 @@ namespace MediaBrowser.Model.Configuration
public long SlowResponseThresholdMs { get; set; }
/// <summary>
+ /// Gets or sets the cors hosts.
+ /// </summary>
+ public string[] CorsHosts { get; set; }
+
+ /// <summary>
+ /// Gets or sets the known proxies.
+ /// </summary>
+ public string[] KnownProxies { get; set; }
+
+ /// <summary>
/// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
/// </summary>
public ServerConfiguration()
@@ -296,7 +304,6 @@ namespace MediaBrowser.Model.Configuration
DisableLiveTvChannelUserDataName = true;
EnableNewOmdbSupport = true;
- AutoRunWebApp = true;
EnableRemoteAccess = true;
QuickConnectAvailable = false;
@@ -372,6 +379,8 @@ namespace MediaBrowser.Model.Configuration
EnableSlowResponseWarning = true;
SlowResponseThresholdMs = 500;
+ CorsHosts = new[] { "*" };
+ KnownProxies = Array.Empty<string>();
}
}
diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
index 85cc9e3c1..43407383a 100644
--- a/MediaBrowser.Model/Dlna/DeviceIdentification.cs
+++ b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
@@ -38,12 +38,6 @@ namespace MediaBrowser.Model.Dlna
public string ModelDescription { get; set; }
/// <summary>
- /// Gets or sets the device description.
- /// </summary>
- /// <value>The device description.</value>
- public string DeviceDescription { get; set; }
-
- /// <summary>
/// Gets or sets the model URL.
/// </summary>
/// <value>The model URL.</value>
diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
index 102db3b44..a4305c810 100644
--- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
+++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
@@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Dlna
new ResolutionConfiguration(720, 950000),
new ResolutionConfiguration(1280, 2500000),
new ResolutionConfiguration(1920, 4000000),
+ new ResolutionConfiguration(2560, 8000000),
new ResolutionConfiguration(3840, 35000000)
};
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index cfe862f5a..d9e7e4fbb 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -498,7 +498,6 @@ namespace MediaBrowser.Model.Dlna
}
}
-
if (playMethods.Count > 0)
{
transcodeReasons.Clear();
@@ -1431,6 +1430,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.AudioChannels:
{
if (string.IsNullOrEmpty(qualifier))
@@ -1466,6 +1466,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.IsAvc:
{
if (!enableNonQualifiedConditions)
@@ -1487,6 +1488,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.IsAnamorphic:
{
if (!enableNonQualifiedConditions)
@@ -1508,6 +1510,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.IsInterlaced:
{
if (string.IsNullOrEmpty(qualifier))
@@ -1539,6 +1542,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.AudioProfile:
case ProfileConditionValue.Has64BitOffsets:
case ProfileConditionValue.PacketLength:
@@ -1550,6 +1554,7 @@ namespace MediaBrowser.Model.Dlna
// Not supported yet
break;
}
+
case ProfileConditionValue.RefFrames:
{
if (string.IsNullOrEmpty(qualifier))
@@ -1585,6 +1590,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.VideoBitDepth:
{
if (string.IsNullOrEmpty(qualifier))
@@ -1620,6 +1626,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.VideoProfile:
{
if (string.IsNullOrEmpty(qualifier))
@@ -1643,6 +1650,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.Height:
{
if (!enableNonQualifiedConditions)
@@ -1668,6 +1676,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.VideoBitrate:
{
if (!enableNonQualifiedConditions)
@@ -1693,6 +1702,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.VideoFramerate:
{
if (!enableNonQualifiedConditions)
@@ -1718,6 +1728,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.VideoLevel:
{
if (string.IsNullOrEmpty(qualifier))
@@ -1743,6 +1754,7 @@ namespace MediaBrowser.Model.Dlna
break;
}
+
case ProfileConditionValue.Width:
{
if (!enableNonQualifiedConditions)
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 94d53ab70..9399d21f1 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -276,7 +276,6 @@ namespace MediaBrowser.Model.Dlna
list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
-
if (!item.IsDirectStream)
{
if (item.RequireNonAnamorphic)
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index af3d83ade..fac754177 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -429,6 +429,7 @@ namespace MediaBrowser.Model.Dto
/// </summary>
/// <value>The album id.</value>
public Guid AlbumId { get; set; }
+
/// <summary>
/// Gets or sets the album image tag.
/// </summary>
@@ -599,11 +600,13 @@ namespace MediaBrowser.Model.Dto
/// </summary>
/// <value>The trailer count.</value>
public int? TrailerCount { get; set; }
+
/// <summary>
/// Gets or sets the movie count.
/// </summary>
/// <value>The movie count.</value>
public int? MovieCount { get; set; }
+
/// <summary>
/// Gets or sets the series count.
/// </summary>
@@ -611,16 +614,19 @@ namespace MediaBrowser.Model.Dto
public int? SeriesCount { get; set; }
public int? ProgramCount { get; set; }
+
/// <summary>
/// Gets or sets the episode count.
/// </summary>
/// <value>The episode count.</value>
public int? EpisodeCount { get; set; }
+
/// <summary>
/// Gets or sets the song count.
/// </summary>
/// <value>The song count.</value>
public int? SongCount { get; set; }
+
/// <summary>
/// Gets or sets the album count.
/// </summary>
@@ -628,6 +634,7 @@ namespace MediaBrowser.Model.Dto
public int? AlbumCount { get; set; }
public int? ArtistCount { get; set; }
+
/// <summary>
/// Gets or sets the music video count.
/// </summary>
@@ -768,6 +775,7 @@ namespace MediaBrowser.Model.Dto
/// </summary>
/// <value>The timer identifier.</value>
public string TimerId { get; set; }
+
/// <summary>
/// Gets or sets the current program.
/// </summary>
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 2d37618c2..fa3c9aaa2 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -451,11 +451,13 @@ namespace MediaBrowser.Model.Entities
/// </summary>
/// <value>The method.</value>
public SubtitleDeliveryMethod? DeliveryMethod { get; set; }
+
/// <summary>
/// Gets or sets the delivery URL.
/// </summary>
/// <value>The delivery URL.</value>
public string DeliveryUrl { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether this instance is external URL.
/// </summary>
diff --git a/MediaBrowser.Model/Entities/MetadataProvider.cs b/MediaBrowser.Model/Entities/MetadataProvider.cs
index 7fecf67b8..e9c098021 100644
--- a/MediaBrowser.Model/Entities/MetadataProvider.cs
+++ b/MediaBrowser.Model/Entities/MetadataProvider.cs
@@ -11,18 +11,22 @@ namespace MediaBrowser.Model.Entities
/// The imdb.
/// </summary>
Imdb = 2,
+
/// <summary>
/// The TMDB.
/// </summary>
Tmdb = 3,
+
/// <summary>
/// The TVDB.
/// </summary>
Tvdb = 4,
+
/// <summary>
/// The tvcom.
/// </summary>
Tvcom = 5,
+
/// <summary>
/// Tmdb Collection Id.
/// </summary>
diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
index ab74aff28..bcba344cc 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
@@ -84,6 +84,7 @@ namespace MediaBrowser.Model.LiveTv
/// </summary>
/// <value><c>null</c> if [is kids] contains no value, <c>true</c> if [is kids]; otherwise, <c>false</c>.</value>
public bool? IsKids { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether this instance is sports.
/// </summary>
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index c0a75009a..264681090 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.7" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.8" />
<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/Providers/RemoteImageInfo.cs b/MediaBrowser.Model/Providers/RemoteImageInfo.cs
index 78ab6c706..fb25999e0 100644
--- a/MediaBrowser.Model/Providers/RemoteImageInfo.cs
+++ b/MediaBrowser.Model/Providers/RemoteImageInfo.cs
@@ -68,5 +68,4 @@ namespace MediaBrowser.Model.Providers
/// <value>The type of the rating.</value>
public RatingType RatingType { get; set; }
}
-
}
diff --git a/MediaBrowser.Model/Providers/RemoteSearchResult.cs b/MediaBrowser.Model/Providers/RemoteSearchResult.cs
index 989741c01..a29e7ad1c 100644
--- a/MediaBrowser.Model/Providers/RemoteSearchResult.cs
+++ b/MediaBrowser.Model/Providers/RemoteSearchResult.cs
@@ -50,6 +50,5 @@ namespace MediaBrowser.Model.Providers
public RemoteSearchResult AlbumArtist { get; set; }
public RemoteSearchResult[] Artists { get; set; }
-
}
}
diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs
index 731d22aaf..ef4698f3f 100644
--- a/MediaBrowser.Model/Querying/ItemFields.cs
+++ b/MediaBrowser.Model/Querying/ItemFields.cs
@@ -168,6 +168,7 @@ namespace MediaBrowser.Model.Querying
Studios,
BasicSyncInfo,
+
/// <summary>
/// The synchronize information.
/// </summary>
diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
index 2ef6f7c60..eb6239460 100644
--- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
+++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
@@ -37,16 +37,19 @@ namespace MediaBrowser.Model.Querying
/// </summary>
/// <value>The fields.</value>
public ItemFields[] Fields { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether [enable images].
/// </summary>
/// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value>
public bool? EnableImages { get; set; }
+
/// <summary>
/// Gets or sets the image type limit.
/// </summary>
/// <value>The image type limit.</value>
public int? ImageTypeLimit { get; set; }
+
/// <summary>
/// Gets or sets the enable image types.
/// </summary>
diff --git a/MediaBrowser.Model/Session/GeneralCommand.cs b/MediaBrowser.Model/Session/GeneralCommand.cs
index 9794bd292..77bb6bcf7 100644
--- a/MediaBrowser.Model/Session/GeneralCommand.cs
+++ b/MediaBrowser.Model/Session/GeneralCommand.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Model.Session
{
public class GeneralCommand
{
- public string Name { get; set; }
+ public GeneralCommandType Name { get; set; }
public Guid ControllingUserId { get; set; }
diff --git a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs
index 21bcabf1d..73dbe6a2d 100644
--- a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs
+++ b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs
@@ -88,16 +88,19 @@ namespace MediaBrowser.Model.Session
/// </summary>
/// <value>The play method.</value>
public PlayMethod PlayMethod { get; set; }
+
/// <summary>
/// Gets or sets the live stream identifier.
/// </summary>
/// <value>The live stream identifier.</value>
public string LiveStreamId { get; set; }
+
/// <summary>
/// Gets or sets the play session identifier.
/// </summary>
/// <value>The play session identifier.</value>
public string PlaySessionId { get; set; }
+
/// <summary>
/// Gets or sets the repeat mode.
/// </summary>
diff --git a/MediaBrowser.Model/Sync/SyncCategory.cs b/MediaBrowser.Model/Sync/SyncCategory.cs
index 80ad5f56e..1248c2f73 100644
--- a/MediaBrowser.Model/Sync/SyncCategory.cs
+++ b/MediaBrowser.Model/Sync/SyncCategory.cs
@@ -8,10 +8,12 @@ namespace MediaBrowser.Model.Sync
/// The latest.
/// </summary>
Latest = 0,
+
/// <summary>
/// The next up.
/// </summary>
NextUp = 1,
+
/// <summary>
/// The resume.
/// </summary>
diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs
index b6196a43f..d2f7556a5 100644
--- a/MediaBrowser.Model/System/PublicSystemInfo.cs
+++ b/MediaBrowser.Model/System/PublicSystemInfo.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Model.System
public string Version { get; set; }
/// <summary>
- /// The product name. This is the AssemblyProduct name.
+ /// Gets or sets the product name. This is the AssemblyProduct name.
/// </summary>
public string ProductName { get; set; }
@@ -39,5 +39,11 @@ namespace MediaBrowser.Model.System
/// </summary>
/// <value>The id.</value>
public string Id { get; set; }
+
+ /// <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; }
}
}
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index 18ca74ee3..4b83fb7e6 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -14,13 +14,16 @@ namespace MediaBrowser.Model.System
{
/// <summary>No path to FFmpeg found.</summary>
NotFound,
+
/// <summary>Path supplied via command line using switch --ffmpeg.</summary>
SetByArgument,
+
/// <summary>User has supplied path via Transcoding UI page.</summary>
Custom,
+
/// <summary>FFmpeg tool found on system $PATH.</summary>
System
- };
+ }
/// <summary>
/// Class SystemInfo.
diff --git a/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs b/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs
index fbfaed22e..6212d76f7 100644
--- a/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs
+++ b/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs
@@ -9,6 +9,7 @@ namespace MediaBrowser.Model.Tasks
/// </summary>
/// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
bool IsHidden { get; }
+
/// <summary>
/// Gets a value indicating whether this instance is enabled.
/// </summary>
diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs
index d9eb1386e..98b151d55 100644
--- a/MediaBrowser.Model/Updates/PackageInfo.cs
+++ b/MediaBrowser.Model/Updates/PackageInfo.cs
@@ -53,6 +53,16 @@ namespace MediaBrowser.Model.Updates
public IReadOnlyList<VersionInfo> versions { get; set; }
/// <summary>
+ /// Gets or sets the repository name.
+ /// </summary>
+ public string repositoryName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the repository url.
+ /// </summary>
+ public string repositoryUrl { get; set; }
+
+ /// <summary>
/// Initializes a new instance of the <see cref="PackageInfo"/> class.
/// </summary>
public PackageInfo()
diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
index 46f368f72..e0f3131fd 100644
--- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
+++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
@@ -1,6 +1,5 @@
#pragma warning disable CS1591
-
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
diff --git a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
index 2e6cf4530..2e6cf4530 100644
--- a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs
+++ b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 413d297cb..19a42d506 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -7,7 +7,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -59,6 +58,16 @@ namespace MediaBrowser.Providers.Manager
_logger = logger;
}
+ private bool EnableExtraThumbsDuplication
+ {
+ get
+ {
+ var config = _config.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
+
+ return config.EnableExtraThumbsDuplication;
+ }
+ }
+
/// <summary>
/// Saves the image.
/// </summary>
@@ -69,7 +78,7 @@ namespace MediaBrowser.Providers.Manager
/// <param name="imageIndex">Index of the image.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">mimeType</exception>
+ /// <exception cref="ArgumentNullException">mimeType.</exception>
public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
{
return SaveImage(item, source, mimeType, type, imageIndex, null, cancellationToken);
@@ -312,7 +321,7 @@ namespace MediaBrowser.Providers.Manager
/// <exception cref="ArgumentNullException">
/// imageIndex
/// or
- /// imageIndex
+ /// imageIndex.
/// </exception>
private ItemImageInfo GetCurrentImage(BaseItem item, ImageType type, int imageIndex)
{
@@ -328,7 +337,8 @@ namespace MediaBrowser.Providers.Manager
/// <param name="path">The path.</param>
/// <exception cref="ArgumentNullException">imageIndex
/// or
- /// imageIndex</exception>
+ /// imageIndex.
+ /// </exception>
private void SetImagePath(BaseItem item, ImageType type, int? imageIndex, string path)
{
item.SetImagePath(type, imageIndex ?? 0, _fileSystem.GetFileInfo(path));
@@ -346,7 +356,7 @@ namespace MediaBrowser.Providers.Manager
/// <exception cref="ArgumentNullException">
/// imageIndex
/// or
- /// imageIndex
+ /// imageIndex.
/// </exception>
private string GetStandardSavePath(BaseItem item, ImageType type, int? imageIndex, string mimeType, bool saveLocally)
{
@@ -500,7 +510,7 @@ namespace MediaBrowser.Providers.Manager
/// <param name="imageIndex">Index of the image.</param>
/// <param name="mimeType">Type of the MIME.</param>
/// <returns>IEnumerable{System.String}.</returns>
- /// <exception cref="ArgumentNullException">imageIndex</exception>
+ /// <exception cref="ArgumentNullException">imageIndex.</exception>
private string[] GetCompatibleSavePaths(BaseItem item, ImageType type, int? imageIndex, string mimeType)
{
var season = item as Season;
@@ -604,16 +614,6 @@ namespace MediaBrowser.Providers.Manager
return new[] { GetStandardSavePath(item, type, imageIndex, mimeType, true) };
}
- private bool EnableExtraThumbsDuplication
- {
- get
- {
- var config = _config.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
-
- return config.EnableExtraThumbsDuplication;
- }
- }
-
/// <summary>
/// Gets the save path for item in mixed folder.
/// </summary>
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 9227b6d93..d0bdbd7c9 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -28,6 +28,22 @@ namespace MediaBrowser.Providers.Manager
private readonly IProviderManager _providerManager;
private readonly IFileSystem _fileSystem;
+ /// <summary>
+ /// Image types that are only one per item.
+ /// </summary>
+ private readonly ImageType[] _singularImages =
+ {
+ ImageType.Primary,
+ ImageType.Art,
+ ImageType.Banner,
+ ImageType.Box,
+ ImageType.BoxRear,
+ ImageType.Disc,
+ ImageType.Logo,
+ ImageType.Menu,
+ ImageType.Thumb
+ };
+
public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem)
{
_logger = logger;
@@ -175,22 +191,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- /// <summary>
- /// Image types that are only one per item.
- /// </summary>
- private readonly ImageType[] _singularImages =
- {
- ImageType.Primary,
- ImageType.Art,
- ImageType.Banner,
- ImageType.Box,
- ImageType.BoxRear,
- ImageType.Disc,
- ImageType.Logo,
- ImageType.Menu,
- ImageType.Thumb
- };
-
private bool HasImage(BaseItem item, ImageType type)
{
return item.HasImage(type);
@@ -378,7 +378,6 @@ namespace MediaBrowser.Providers.Manager
}
else
{
-
var newDateModified = _fileSystem.GetLastWriteTimeUtc(image.FileInfo);
// If date changed then we need to reset saved image dimensions
@@ -441,7 +440,9 @@ namespace MediaBrowser.Providers.Manager
return changed;
}
- private async Task<bool> DownloadImage(BaseItem item, LibraryOptions libraryOptions,
+ private async Task<bool> DownloadImage(
+ BaseItem item,
+ LibraryOptions libraryOptions,
IRemoteImageProvider provider,
RefreshResult result,
IEnumerable<RemoteImageInfo> images,
@@ -522,11 +523,6 @@ namespace MediaBrowser.Providers.Manager
return false;
}
- // if (!item.IsSaveLocalMetadataEnabled())
- //{
- // return true;
- //}
-
return true;
}
@@ -539,13 +535,15 @@ namespace MediaBrowser.Providers.Manager
private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls, int newIndex)
{
- var path = string.Join("|", urls.Take(1).ToArray());
+ var path = string.Join('|', urls.Take(1));
- item.SetImage(new ItemImageInfo
- {
- Path = path,
- Type = imageType
- }, newIndex);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = path,
+ Type = imageType
+ },
+ newIndex);
}
private async Task DownloadBackdrops(BaseItem item, LibraryOptions libraryOptions, ImageType imageType, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index d0de58427..f110eafa5 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -21,12 +21,6 @@ namespace MediaBrowser.Providers.Manager
where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
where TIdType : ItemLookupInfo, new()
{
- protected readonly IServerConfigurationManager ServerConfigurationManager;
- protected readonly ILogger<MetadataService<TItemType, TIdType>> Logger;
- protected readonly IProviderManager ProviderManager;
- protected readonly IFileSystem FileSystem;
- protected readonly ILibraryManager LibraryManager;
-
protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager)
{
ServerConfigurationManager = serverConfigurationManager;
@@ -36,6 +30,26 @@ namespace MediaBrowser.Providers.Manager
LibraryManager = libraryManager;
}
+ protected IServerConfigurationManager ServerConfigurationManager { get; }
+
+ protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; }
+
+ protected IProviderManager ProviderManager { get; }
+
+ protected IFileSystem FileSystem { get; }
+
+ protected ILibraryManager LibraryManager { get; }
+
+ protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
+
+ protected virtual bool EnableUpdatingGenresFromChildren => false;
+
+ protected virtual bool EnableUpdatingStudiosFromChildren => false;
+
+ protected virtual bool EnableUpdatingOfficialRatingFromChildren => false;
+
+ public virtual int Order => 0;
+
private FileSystemMetadata TryGetFile(string path, IDirectoryService directoryService)
{
try
@@ -283,7 +297,7 @@ namespace MediaBrowser.Providers.Manager
}
/// <summary>
- /// Befores the save.
+ /// Before the save.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
@@ -341,13 +355,12 @@ namespace MediaBrowser.Providers.Manager
protected virtual IList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
{
- var folder = item as Folder;
- if (folder != null)
+ if (item is Folder folder)
{
return folder.GetRecursiveChildren();
}
- return new List<BaseItem>();
+ return Array.Empty<BaseItem>();
}
protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
@@ -442,14 +455,6 @@ namespace MediaBrowser.Providers.Manager
return updateType;
}
- protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
-
- protected virtual bool EnableUpdatingGenresFromChildren => false;
-
- protected virtual bool EnableUpdatingStudiosFromChildren => false;
-
- protected virtual bool EnableUpdatingOfficialRatingFromChildren => false;
-
private ItemUpdateType UpdatePremiereDate(TItemType item, IList<BaseItem> children)
{
var updateType = ItemUpdateType.None;
@@ -658,7 +663,8 @@ namespace MediaBrowser.Providers.Manager
return type == typeof(TItemType);
}
- protected virtual async Task<RefreshResult> RefreshWithProviders(MetadataResult<TItemType> metadata,
+ protected virtual async Task<RefreshResult> RefreshWithProviders(
+ MetadataResult<TItemType> metadata,
TIdType id,
MetadataRefreshOptions options,
List<IMetadataProvider> providers,
@@ -773,7 +779,7 @@ namespace MediaBrowser.Providers.Manager
else
{
// TODO: If the new metadata from above has some blank data, this can cause old data to get filled into those empty fields
- MergeData(metadata, temp, new MetadataField[] { }, false, false);
+ MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
MergeData(temp, metadata, item.LockedFields, true, false);
}
}
@@ -807,7 +813,7 @@ namespace MediaBrowser.Providers.Manager
try
{
- refreshResult.UpdateType = refreshResult.UpdateType | await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false);
+ refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -875,16 +881,6 @@ namespace MediaBrowser.Providers.Manager
return refreshResult;
}
- private string NormalizeLanguage(string language)
- {
- if (string.IsNullOrWhiteSpace(language))
- {
- return "en";
- }
-
- return language;
- }
-
private void MergeNewData(TItemType source, TIdType lookupInfo)
{
// Copy new provider id's that may have been obtained
@@ -900,24 +896,23 @@ namespace MediaBrowser.Providers.Manager
}
}
- protected abstract void MergeData(MetadataResult<TItemType> source,
+ protected abstract void MergeData(
+ MetadataResult<TItemType> source,
MetadataResult<TItemType> target,
MetadataField[] lockedFields,
bool replaceData,
bool mergeMetadataSettings);
- public virtual int Order => 0;
-
private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService)
{
try
{
var hasChanged = changeMonitor.HasChanged(item, directoryService);
- // if (hasChanged)
- //{
- // logger.LogDebug("{0} reports change to {1}", changeMonitor.GetType().Name, item.Path ?? item.Name);
- //}
+ if (hasChanged)
+ {
+ Logger.LogDebug("{0} reports change to {1}", changeMonitor.GetType().Name, item.Path ?? item.Name);
+ }
return hasChanged;
}
@@ -928,13 +923,4 @@ namespace MediaBrowser.Providers.Manager
}
}
}
-
- public class RefreshResult
- {
- public ItemUpdateType UpdateType { get; set; }
-
- public string ErrorMessage { get; set; }
-
- public int Failures { get; set; }
- }
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 171b824ca..b6fb4267f 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -9,7 +9,6 @@ using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
@@ -905,8 +904,7 @@ namespace MediaBrowser.Providers.Manager
return provider.GetImageResponse(url, cancellationToken);
}
- /// <inheritdoc/>
- public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
+ private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
{
return _externalIds.Where(i =>
{
diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs
index a4fd6ca84..70a5a6ac1 100644
--- a/MediaBrowser.Providers/Manager/ProviderUtils.cs
+++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs
@@ -26,12 +26,12 @@ namespace MediaBrowser.Providers.Manager
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ throw new ArgumentException("Item cannot be null.", nameof(sourceResult));
}
if (target == null)
{
- throw new ArgumentNullException(nameof(target));
+ throw new ArgumentException("Item cannot be null.", nameof(targetResult));
}
if (!lockedFields.Contains(MetadataField.Name))
diff --git a/MediaBrowser.Providers/Manager/RefreshResult.cs b/MediaBrowser.Providers/Manager/RefreshResult.cs
new file mode 100644
index 000000000..72fc61e42
--- /dev/null
+++ b/MediaBrowser.Providers/Manager/RefreshResult.cs
@@ -0,0 +1,15 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Providers.Manager
+{
+ public class RefreshResult
+ {
+ public ItemUpdateType UpdateType { get; set; }
+
+ public string ErrorMessage { get; set; }
+
+ public int Failures { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 39f93c479..813dd441f 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,12 +16,12 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.7" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
+ <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="PlaylistsNET" Version="1.1.2" />
- <PackageReference Include="TvDbSharper" Version="3.2.1" />
+ <PackageReference Include="TvDbSharper" Version="3.2.2" />
</ItemGroup>
<PropertyGroup>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index f69ec9744..64ad1bddf 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -34,6 +34,10 @@ namespace MediaBrowser.Providers.MediaInfo
_fileSystem = fileSystem;
}
+ public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
+
+ public string Name => "Image Extractor";
+
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType> { ImageType.Primary };
@@ -97,11 +101,11 @@ namespace MediaBrowser.Providers.MediaInfo
if (item.GetType() == typeof(Audio))
{
- var albumArtist = item.AlbumArtists.FirstOrDefault();
-
- if (!string.IsNullOrWhiteSpace(item.Album) && !string.IsNullOrWhiteSpace(albumArtist))
+ if (item.AlbumArtists.Count > 0
+ && !string.IsNullOrWhiteSpace(item.Album)
+ && !string.IsNullOrWhiteSpace(item.AlbumArtists[0]))
{
- filename = (item.Album + "-" + albumArtist).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ filename = (item.Album + "-" + item.AlbumArtists[0]).GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
else
{
@@ -121,10 +125,6 @@ namespace MediaBrowser.Providers.MediaInfo
return Path.Join(AudioImagesPath, prefix, filename);
}
- public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
-
- public string Name => "Image Extractor";
-
public bool Supports(BaseItem item)
{
if (item.IsShortcut)
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
index 77f03580a..945463666 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
@@ -37,7 +37,9 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaSourceManager = mediaSourceManager;
}
- public async Task<ItemUpdateType> Probe<T>(T item, MetadataRefreshOptions options,
+ public async Task<ItemUpdateType> Probe<T>(
+ T item,
+ MetadataRefreshOptions options,
CancellationToken cancellationToken)
where T : Audio
{
@@ -52,19 +54,21 @@ namespace MediaBrowser.Providers.MediaInfo
protocol = _mediaSourceManager.GetPathProtocol(path);
}
- var result = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaType = DlnaProfileType.Audio,
- MediaSource = new MediaSourceInfo
+ var result = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
{
- Path = path,
- Protocol = protocol
- }
- }, cancellationToken).ConfigureAwait(false);
+ MediaType = DlnaProfileType.Audio,
+ MediaSource = new MediaSourceInfo
+ {
+ Path = path,
+ Protocol = protocol
+ }
+ },
+ cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
- Fetch(item, cancellationToken, result);
+ Fetch(item, result, cancellationToken);
}
return ItemUpdateType.MetadataImport;
@@ -74,10 +78,9 @@ namespace MediaBrowser.Providers.MediaInfo
/// Fetches the specified audio.
/// </summary>
/// <param name="audio">The audio.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="mediaInfo">The media information.</param>
- /// <returns>Task.</returns>
- protected void Fetch(Audio audio, CancellationToken cancellationToken, Model.MediaInfo.MediaInfo mediaInfo)
+ /// <param name="cancellationToken">The cancellation token.</param>
+ protected void Fetch(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, CancellationToken cancellationToken)
{
var mediaStreams = mediaInfo.MediaStreams;
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
index 9926275ae..c61187fdf 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
@@ -5,8 +5,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -20,9 +18,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo
@@ -50,9 +46,43 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly IChapterManager _chapterManager;
private readonly ILibraryManager _libraryManager;
private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly SubtitleResolver _subtitleResolver;
+
+ private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
+
+ public FFProbeProvider(
+ ILogger<FFProbeProvider> logger,
+ IMediaSourceManager mediaSourceManager,
+ IMediaEncoder mediaEncoder,
+ IItemRepository itemRepo,
+ IBlurayExaminer blurayExaminer,
+ ILocalizationManager localization,
+ IEncodingManager encodingManager,
+ IServerConfigurationManager config,
+ ISubtitleManager subtitleManager,
+ IChapterManager chapterManager,
+ ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _mediaEncoder = mediaEncoder;
+ _itemRepo = itemRepo;
+ _blurayExaminer = blurayExaminer;
+ _localization = localization;
+ _encodingManager = encodingManager;
+ _config = config;
+ _subtitleManager = subtitleManager;
+ _chapterManager = chapterManager;
+ _libraryManager = libraryManager;
+ _mediaSourceManager = mediaSourceManager;
+
+ _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
+ }
public string Name => "ffprobe";
+ // Run last
+ public int Order => 100;
+
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
var video = item as Video;
@@ -117,37 +147,6 @@ namespace MediaBrowser.Providers.MediaInfo
return FetchAudioInfo(item, options, cancellationToken);
}
- private SubtitleResolver _subtitleResolver;
-
- public FFProbeProvider(
- ILogger<FFProbeProvider> logger,
- IMediaSourceManager mediaSourceManager,
- IMediaEncoder mediaEncoder,
- IItemRepository itemRepo,
- IBlurayExaminer blurayExaminer,
- ILocalizationManager localization,
- IEncodingManager encodingManager,
- IServerConfigurationManager config,
- ISubtitleManager subtitleManager,
- IChapterManager chapterManager,
- ILibraryManager libraryManager)
- {
- _logger = logger;
- _mediaEncoder = mediaEncoder;
- _itemRepo = itemRepo;
- _blurayExaminer = blurayExaminer;
- _localization = localization;
- _encodingManager = encodingManager;
- _config = config;
- _subtitleManager = subtitleManager;
- _chapterManager = chapterManager;
- _libraryManager = libraryManager;
- _mediaSourceManager = mediaSourceManager;
-
- _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
- }
-
- private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
public Task<ItemUpdateType> FetchVideoInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
where T : Video
{
@@ -234,8 +233,5 @@ namespace MediaBrowser.Providers.MediaInfo
return prober.Probe(item, options, cancellationToken);
}
-
- // Run last
- public int Order => 100;
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 53a6bb619..776dee780 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -539,17 +539,18 @@ namespace MediaBrowser.Providers.MediaInfo
if (enableSubtitleDownloading && enabled)
{
- var downloadedLanguages = await new SubtitleDownloader(_logger,
- _subtitleManager)
- .DownloadSubtitles(video,
- currentStreams.Concat(externalSubtitleStreams).ToList(),
- skipIfEmbeddedSubtitlesPresent,
- skipIfAudioTrackMatches,
- requirePerfectMatch,
- subtitleDownloadLanguages,
- libraryOptions.DisabledSubtitleFetchers,
- libraryOptions.SubtitleFetcherOrder,
- cancellationToken).ConfigureAwait(false);
+ var downloadedLanguages = await new SubtitleDownloader(
+ _logger,
+ _subtitleManager).DownloadSubtitles(
+ video,
+ currentStreams.Concat(externalSubtitleStreams).ToList(),
+ skipIfEmbeddedSubtitlesPresent,
+ skipIfAudioTrackMatches,
+ requirePerfectMatch,
+ subtitleDownloadLanguages,
+ libraryOptions.DisabledSubtitleFetchers,
+ libraryOptions.SubtitleFetcherOrder,
+ cancellationToken).ConfigureAwait(false);
// Rescan
if (downloadedLanguages.Count > 0)
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
index acddb73d0..912aedb0d 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
@@ -42,8 +42,16 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var lang in languages)
{
- var downloaded = await DownloadSubtitles(video, mediaStreams, skipIfEmbeddedSubtitlesPresent,
- skipIfAudioTrackMatches, requirePerfectMatch, lang, disabledSubtitleFetchers, subtitleFetcherOrder, cancellationToken).ConfigureAwait(false);
+ var downloaded = await DownloadSubtitles(
+ video,
+ mediaStreams,
+ skipIfEmbeddedSubtitlesPresent,
+ skipIfAudioTrackMatches,
+ requirePerfectMatch,
+ lang,
+ disabledSubtitleFetchers,
+ subtitleFetcherOrder,
+ cancellationToken).ConfigureAwait(false);
if (downloaded)
{
@@ -54,7 +62,8 @@ namespace MediaBrowser.Providers.MediaInfo
return downloadedLanguages;
}
- public Task<bool> DownloadSubtitles(Video video,
+ public Task<bool> DownloadSubtitles(
+ Video video,
List<MediaStream> mediaStreams,
bool skipIfEmbeddedSubtitlesPresent,
bool skipIfAudioTrackMatches,
@@ -90,11 +99,21 @@ namespace MediaBrowser.Providers.MediaInfo
return Task.FromResult(false);
}
- return DownloadSubtitles(video, mediaStreams, skipIfEmbeddedSubtitlesPresent, skipIfAudioTrackMatches,
- requirePerfectMatch, lang, disabledSubtitleFetchers, subtitleFetcherOrder, mediaType, cancellationToken);
+ return DownloadSubtitles(
+ video,
+ mediaStreams,
+ skipIfEmbeddedSubtitlesPresent,
+ skipIfAudioTrackMatches,
+ requirePerfectMatch,
+ lang,
+ disabledSubtitleFetchers,
+ subtitleFetcherOrder,
+ mediaType,
+ cancellationToken);
}
- private async Task<bool> DownloadSubtitles(Video video,
+ private async Task<bool> DownloadSubtitles(
+ Video video,
List<MediaStream> mediaStreams,
bool skipIfEmbeddedSubtitlesPresent,
bool skipIfAudioTrackMatches,
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index 43659b68c..e9f999c6d 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -66,9 +66,10 @@ namespace MediaBrowser.Providers.MediaInfo
return streams;
}
- public List<string> GetExternalSubtitleFiles(Video video,
- IDirectoryService directoryService,
- bool clearCache)
+ public List<string> GetExternalSubtitleFiles(
+ Video video,
+ IDirectoryService directoryService,
+ bool clearCache)
{
var list = new List<string>();
@@ -87,7 +88,9 @@ namespace MediaBrowser.Providers.MediaInfo
return list;
}
- private void AddExternalSubtitleStreams(List<MediaStream> streams, string folder,
+ private void AddExternalSubtitleStreams(
+ List<MediaStream> streams,
+ string folder,
string videoPath,
int startIndex,
IDirectoryService directoryService,
@@ -98,7 +101,8 @@ namespace MediaBrowser.Providers.MediaInfo
AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
}
- public void AddExternalSubtitleStreams(List<MediaStream> streams,
+ public void AddExternalSubtitleStreams(
+ List<MediaStream> streams,
string videoPath,
int startIndex,
string[] files)
@@ -185,8 +189,8 @@ namespace MediaBrowser.Providers.MediaInfo
private string NormalizeFilenameForSubtitleComparison(string filename)
{
// Try to account for sloppy file naming
- filename = filename.Replace("_", string.Empty);
- filename = filename.Replace(" ", string.Empty);
+ filename = filename.Replace("_", string.Empty, StringComparison.Ordinal);
+ filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal);
// can't normalize this due to languages such as pt-br
// filename = filename.Replace("-", string.Empty);
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 91ab7b4ac..d231bfa2f 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -12,11 +12,10 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
namespace MediaBrowser.Providers.MediaInfo
{
@@ -25,29 +24,37 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
private readonly ISubtitleManager _subtitleManager;
- private readonly IMediaSourceManager _mediaSourceManager;
private readonly ILogger<SubtitleScheduledTask> _logger;
- private readonly IJsonSerializer _json;
private readonly ILocalizationManager _localization;
public SubtitleScheduledTask(
ILibraryManager libraryManager,
- IJsonSerializer json,
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
ILogger<SubtitleScheduledTask> logger,
- IMediaSourceManager mediaSourceManager,
ILocalizationManager localization)
{
_libraryManager = libraryManager;
_config = config;
_subtitleManager = subtitleManager;
_logger = logger;
- _mediaSourceManager = mediaSourceManager;
- _json = json;
_localization = localization;
}
+ public string Name => _localization.GetLocalizedString("TaskDownloadMissingSubtitles");
+
+ public string Description => _localization.GetLocalizedString("TaskDownloadMissingSubtitlesDescription");
+
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ public string Key => "DownloadSubtitles";
+
+ public bool IsHidden => false;
+
+ public bool IsEnabled => true;
+
+ public bool IsLogged => true;
+
private SubtitleOptions GetOptions()
{
return _config.GetConfiguration<SubtitleOptions>("subtitles");
@@ -66,23 +73,23 @@ namespace MediaBrowser.Providers.MediaInfo
var libraryOptions = _libraryManager.GetLibraryOptions(library);
string[] subtitleDownloadLanguages;
- bool SkipIfEmbeddedSubtitlesPresent;
- bool SkipIfAudioTrackMatches;
- bool RequirePerfectMatch;
+ bool skipIfEmbeddedSubtitlesPresent;
+ bool skipIfAudioTrackMatches;
+ bool requirePerfectMatch;
if (libraryOptions.SubtitleDownloadLanguages == null)
{
subtitleDownloadLanguages = options.DownloadLanguages;
- SkipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
- SkipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
- RequirePerfectMatch = options.RequirePerfectMatch;
+ skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
+ skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
+ requirePerfectMatch = options.RequirePerfectMatch;
}
else
{
subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
- SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
- SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
- RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
+ skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
+ skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
+ requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
}
foreach (var lang in subtitleDownloadLanguages)
@@ -98,12 +105,12 @@ namespace MediaBrowser.Providers.MediaInfo
Recursive = true
};
- if (SkipIfAudioTrackMatches)
+ if (skipIfAudioTrackMatches)
{
query.HasNoAudioTrackWithLanguage = lang;
}
- if (SkipIfEmbeddedSubtitlesPresent)
+ if (skipIfEmbeddedSubtitlesPresent)
{
// Exclude if it already has any subtitles of the same language
query.HasNoSubtitleTrackWithLanguage = lang;
@@ -160,36 +167,37 @@ namespace MediaBrowser.Providers.MediaInfo
var libraryOptions = _libraryManager.GetLibraryOptions(video);
string[] subtitleDownloadLanguages;
- bool SkipIfEmbeddedSubtitlesPresent;
- bool SkipIfAudioTrackMatches;
- bool RequirePerfectMatch;
+ bool skipIfEmbeddedSubtitlesPresent;
+ bool skipIfAudioTrackMatches;
+ bool requirePerfectMatch;
if (libraryOptions.SubtitleDownloadLanguages == null)
{
subtitleDownloadLanguages = options.DownloadLanguages;
- SkipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
- SkipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
- RequirePerfectMatch = options.RequirePerfectMatch;
+ skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
+ skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
+ requirePerfectMatch = options.RequirePerfectMatch;
}
else
{
subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
- SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
- SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
- RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
+ skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
+ skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
+ requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
}
- var downloadedLanguages = await new SubtitleDownloader(_logger,
- _subtitleManager)
- .DownloadSubtitles(video,
- mediaStreams,
- SkipIfEmbeddedSubtitlesPresent,
- SkipIfAudioTrackMatches,
- RequirePerfectMatch,
- subtitleDownloadLanguages,
- libraryOptions.DisabledSubtitleFetchers,
- libraryOptions.SubtitleFetcherOrder,
- cancellationToken).ConfigureAwait(false);
+ var downloadedLanguages = await new SubtitleDownloader(
+ _logger,
+ _subtitleManager).DownloadSubtitles(
+ video,
+ mediaStreams,
+ skipIfEmbeddedSubtitlesPresent,
+ skipIfAudioTrackMatches,
+ requirePerfectMatch,
+ subtitleDownloadLanguages,
+ libraryOptions.DisabledSubtitleFetchers,
+ libraryOptions.SubtitleFetcherOrder,
+ cancellationToken).ConfigureAwait(false);
// Rescan
if (downloadedLanguages.Count > 0)
@@ -203,25 +211,11 @@ namespace MediaBrowser.Providers.MediaInfo
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
- return new[] {
-
+ return new[]
+ {
// Every so often
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
};
}
-
- public string Name => _localization.GetLocalizedString("TaskDownloadMissingSubtitles");
-
- public string Description => _localization.GetLocalizedString("TaskDownloadMissingSubtitlesDescription");
-
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
-
- public string Key => "DownloadSubtitles";
-
- public bool IsHidden => false;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
index e23854d90..fc38d3832 100644
--- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
@@ -29,6 +29,11 @@ namespace MediaBrowser.Providers.MediaInfo
_fileSystem = fileSystem;
}
+ public string Name => "Screen Grabber";
+
+ // Make sure this comes after internet image providers
+ public int Order => 100;
+
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType> { ImageType.Primary };
@@ -127,8 +132,6 @@ namespace MediaBrowser.Providers.MediaInfo
};
}
- public string Name => "Screen Grabber";
-
public bool Supports(BaseItem item)
{
if (item.IsShortcut)
@@ -150,7 +153,5 @@ namespace MediaBrowser.Providers.MediaInfo
return false;
}
- // Make sure this comes after internet image providers
- public int Order => 100;
}
}
diff --git a/MediaBrowser.Providers/Movies/MovieExternalIds.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
index 14080841c..a8d74aa0b 100644
--- a/MediaBrowser.Providers/Movies/MovieExternalIds.cs
+++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
@@ -36,22 +36,4 @@ namespace MediaBrowser.Providers.Movies
return item is Movie || item is MusicVideo || item is Series || item is Episode || item is Trailer;
}
}
-
- public class ImdbPersonExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "IMDb";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.Imdb.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
-
- /// <inheritdoc />
- public string UrlFormatString => "https://www.imdb.com/name/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Person;
- }
}
diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
new file mode 100644
index 000000000..8151ab471
--- /dev/null
+++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Movies
+{
+ public class ImdbPersonExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "IMDb";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.Imdb.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
+
+ /// <inheritdoc />
+ public string UrlFormatString => "https://www.imdb.com/name/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Person;
+ }
+}
diff --git a/MediaBrowser.Providers/Music/Extensions.cs b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
index dddfd02e4..dddfd02e4 100644
--- a/MediaBrowser.Providers/Music/Extensions.cs
+++ b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
diff --git a/MediaBrowser.Providers/Music/MusicExternalIds.cs b/MediaBrowser.Providers/Music/ImvdbId.cs
index a1726b996..a1726b996 100644
--- a/MediaBrowser.Providers/Music/MusicExternalIds.cs
+++ b/MediaBrowser.Providers/Music/ImvdbId.cs
diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
index 5cc0a527e..067d585cb 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
@@ -10,7 +10,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using PlaylistsNET.Content;
@@ -23,16 +22,17 @@ namespace MediaBrowser.Providers.Playlists
IHasItemChangeMonitor
{
private readonly ILogger<PlaylistItemsProvider> _logger;
- private IFileSystem _fileSystem;
- public PlaylistItemsProvider(IFileSystem fileSystem, ILogger<PlaylistItemsProvider> logger)
+ public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger)
{
- _fileSystem = fileSystem;
_logger = logger;
}
public string Name => "Playlist Reader";
+ // Run last
+ public int Order => 100;
+
public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
var path = item.Path;
@@ -163,7 +163,5 @@ namespace MediaBrowser.Providers.Playlists
return false;
}
- // Run last
- public int Order => 100;
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
index 670c0cd05..72dad8a25 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
@@ -23,16 +23,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
public class AudioDbArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IHasOrder
{
+ private const string ApiKey = "195003";
+ public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey;
+
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _json;
- public static AudioDbArtistProvider Current;
-
- private const string ApiKey = "195003";
- public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey;
-
public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json)
{
_config = config;
@@ -42,6 +40,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Current = this;
}
+ public static AudioDbArtistProvider Current { get; private set; }
+
/// <inheritdoc />
public string Name => "TheAudioDB";
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
new file mode 100644
index 000000000..138cfef19
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb
+{
+ public class AudioDbAlbumExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "TheAudioDb";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.AudioDbAlbum.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => null;
+
+ /// <inheritdoc />
+ public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is MusicAlbum;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
new file mode 100644
index 000000000..8aceb48c0
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb
+{
+ public class AudioDbArtistExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "TheAudioDb";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.AudioDbArtist.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+
+ /// <inheritdoc />
+ public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is MusicArtist;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
new file mode 100644
index 000000000..014481da2
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb
+{
+ public class AudioDbOtherAlbumExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "TheAudioDb";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.AudioDbAlbum.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+
+ /// <inheritdoc />
+ public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
new file mode 100644
index 000000000..787539104
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.AudioDb
+{
+ public class AudioDbOtherArtistExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "TheAudioDb";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.AudioDbArtist.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+
+ /// <inheritdoc />
+ public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs b/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs
deleted file mode 100644
index 1cc1f0fa1..000000000
--- a/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-
-namespace MediaBrowser.Providers.Plugins.AudioDb
-{
- public class AudioDbAlbumExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "TheAudioDb";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.AudioDbAlbum.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => null;
-
- /// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is MusicAlbum;
- }
-
- public class AudioDbOtherAlbumExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "TheAudioDb";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.AudioDbAlbum.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
-
- /// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio;
- }
-
- public class AudioDbArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "TheAudioDb";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.AudioDbArtist.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
-
- /// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is MusicArtist;
- }
-
- public class AudioDbOtherArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "TheAudioDb";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.AudioDbArtist.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
-
- /// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
index 54054d015..b5bd72ff0 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
@@ -11,6 +11,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
public static Plugin Instance { get; private set; }
public override Guid Id => new Guid("a629c0da-fac5-4c7e-931a-7174223f14c8");
@@ -22,12 +28,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml";
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
- : base(applicationPaths, xmlSerializer)
- {
- Instance = this;
- }
-
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
index 781b71640..f27da7ce6 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
@@ -198,6 +198,7 @@ namespace MediaBrowser.Providers.Music
result.Name = reader.ReadElementContentAsString();
break;
}
+
case "annotation":
{
result.Overview = reader.ReadElementContentAsString();
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 8414c9328..abfa1c6e7 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -8,7 +8,6 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -27,21 +26,19 @@ namespace MediaBrowser.Providers.Music
public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder
{
/// <summary>
- /// The Jellyfin user-agent is unrestricted but source IP must not exceed
- /// one request per second, therefore we rate limit to avoid throttling.
- /// Be prudent, use a value slightly above the minimun required.
- /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
- /// </summary>
- private readonly long _musicBrainzQueryIntervalMs;
-
- /// <summary>
/// For each single MB lookup/search, this is the maximum number of
/// attempts that shall be made whilst receiving a 503 Server
/// Unavailable (indicating throttled) response.
/// </summary>
private const uint MusicBrainzQueryAttempts = 5u;
- internal static MusicBrainzAlbumProvider Current;
+ /// <summary>
+ /// The Jellyfin user-agent is unrestricted but source IP must not exceed
+ /// one request per second, therefore we rate limit to avoid throttling.
+ /// Be prudent, use a value slightly above the minimun required.
+ /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
+ /// </summary>
+ private readonly long _musicBrainzQueryIntervalMs;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IApplicationHost _appHost;
@@ -69,6 +66,8 @@ namespace MediaBrowser.Providers.Music
Current = this;
}
+ internal static MusicBrainzAlbumProvider Current { get; private set; }
+
/// <inheritdoc />
public string Name => "MusicBrainz";
@@ -112,7 +111,7 @@ namespace MediaBrowser.Providers.Music
else
{
// I'm sure there is a better way but for now it resolves search for 12" Mixes
- var queryName = searchInfo.Name.Replace("\"", string.Empty);
+ var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal);
url = string.Format(
CultureInfo.InvariantCulture,
@@ -277,7 +276,9 @@ namespace MediaBrowser.Providers.Music
private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken)
{
- var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/release/?query=\"{0}\" AND arid:{1}",
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ "/ws/2/release/?query=\"{0}\" AND arid:{1}",
WebUtility.UrlEncode(albumName),
artistId);
@@ -443,6 +444,7 @@ namespace MediaBrowser.Providers.Music
result.Title = reader.ReadElementContentAsString();
break;
}
+
case "date":
{
var val = reader.ReadElementContentAsString();
@@ -453,17 +455,20 @@ namespace MediaBrowser.Providers.Music
break;
}
+
case "annotation":
{
result.Overview = reader.ReadElementContentAsString();
break;
}
+
case "release-group":
{
result.ReleaseGroupId = reader.GetAttribute("id");
reader.Skip();
break;
}
+
case "artist-credit":
{
using (var subReader = reader.ReadSubtree())
@@ -496,7 +501,7 @@ namespace MediaBrowser.Providers.Music
}
}
- private static ValueTuple<string, string> ParseArtistCredit(XmlReader reader)
+ private static (string, string) ParseArtistCredit(XmlReader reader)
{
reader.MoveToContent();
reader.Read();
@@ -531,7 +536,7 @@ namespace MediaBrowser.Providers.Music
}
}
- return new ValueTuple<string, string>();
+ return default;
}
private static (string, string) ParseArtistNameCredit(XmlReader reader)
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
index 8b8fea09e..8f4240dc1 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
@@ -36,6 +36,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
_appHost = appHost;
}
+ public string Name => "The Open Movie Database";
+
+ // After other internet providers, because they're better
+ // But before fallback providers like screengrab
+ public int Order => 90;
+
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType>
@@ -86,15 +92,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
- public string Name => "The Open Movie Database";
-
public bool Supports(BaseItem item)
{
return item is Movie || item is Trailer || item is Episode;
}
-
- // After other internet providers, because they're better
- // But before fallback providers like screengrab
- public int Order => 90;
}
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index d53eba7e9..705359d2c 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -49,6 +49,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
_appHost = appHost;
}
+ public string Name => "The Open Movie Database";
+
// After primary option
public int Order => 2;
@@ -199,8 +201,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return GetSearchResults(searchInfo, "movie", cancellationToken);
}
- public string Name => "The Open Movie Database";
-
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<Series>
@@ -263,14 +263,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var results = await GetSearchResultsInternal(info, "movie", false, cancellationToken).ConfigureAwait(false);
var first = results.FirstOrDefault();
- return first == null ? null : first.GetProviderId(MetadataProvider.Imdb);
+ return first?.GetProviderId(MetadataProvider.Imdb);
}
private async Task<string> GetSeriesImdbId(SeriesInfo info, CancellationToken cancellationToken)
{
var results = await GetSearchResultsInternal(info, "series", false, cancellationToken).ConfigureAwait(false);
var first = results.FirstOrDefault();
- return first == null ? null : first.GetProviderId(MetadataProvider.Imdb);
+ return first?.GetProviderId(MetadataProvider.Imdb);
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
@@ -278,7 +278,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
- class SearchResult
+ private class SearchResult
{
public string Title { get; set; }
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
index de2f6875f..50a876d6c 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
@@ -57,21 +57,28 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
// Process images
try
{
- var episodeInfo = new EpisodeInfo
+ string episodeTvdbId = null;
+
+ if (episode.IndexNumber.HasValue && episode.ParentIndexNumber.HasValue)
{
- IndexNumber = episode.IndexNumber.Value,
- ParentIndexNumber = episode.ParentIndexNumber.Value,
- SeriesProviderIds = series.ProviderIds,
- SeriesDisplayOrder = series.DisplayOrder
- };
- string episodeTvdbId = await _tvdbClientManager
- .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false);
+ var episodeInfo = new EpisodeInfo
+ {
+ IndexNumber = episode.IndexNumber.Value,
+ ParentIndexNumber = episode.ParentIndexNumber.Value,
+ SeriesProviderIds = series.ProviderIds,
+ SeriesDisplayOrder = series.DisplayOrder
+ };
+
+ episodeTvdbId = await _tvdbClientManager
+ .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false);
+ }
+
if (string.IsNullOrEmpty(episodeTvdbId))
{
_logger.LogError(
"Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
- episodeInfo.ParentIndexNumber,
- episodeInfo.IndexNumber,
+ episode.ParentIndexNumber,
+ episode.IndexNumber,
series.GetProviderId(MetadataProvider.Tvdb));
return imageResult;
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
index c088d8cec..5fa8a3e1c 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
@@ -141,6 +141,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
Name = episode.EpisodeName,
Overview = episode.Overview,
CommunityRating = (float?)episode.SiteRating,
+ OfficialRating = episode.ContentRating,
}
};
result.ResetPeople();
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index e627550f1..e7328b553 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -37,7 +37,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
private readonly IJsonSerializer _json;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
- private readonly ILocalizationManager _localization;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
@@ -46,7 +45,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
IJsonSerializer json,
IServerConfigurationManager config,
IFileSystem fileSystem,
- ILocalizationManager localization,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager)
{
@@ -54,7 +52,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
_json = json;
_config = config;
_fileSystem = fileSystem;
- _localization = localization;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
Current = this;
@@ -177,7 +174,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
private async Task<CollectionResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
{
- var url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey);
+ var url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey);
if (!string.IsNullOrEmpty(language))
{
@@ -195,7 +192,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+ 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);
@@ -205,7 +202,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
{
- url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+ url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
if (!string.IsNullOrEmpty(language))
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
index 241dcab4d..1c673fdbd 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
@@ -6,6 +6,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Videos
{
- public List<Video> Results { get; set; }
+ public IReadOnlyList<Video> Results { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
index 166860f51..057177294 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
@@ -6,6 +6,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class Trailers
{
- public List<Youtube> Youtube { get; set; }
+ public IReadOnlyList<Youtube> Youtube { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
index 3ea12334e..d82e0fc6d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
@@ -7,6 +7,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
{
public class PersonImages
{
- public List<Profile> Profiles { get; set; }
+ public IReadOnlyList<Profile> Profiles { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
index 01a887eed..3c626f9eb 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
@@ -302,7 +302,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", i.Source),
Name = i.Name
-
}).ToArray();
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
index a975fb8f6..9db7e0997 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
@@ -38,6 +38,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
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;
@@ -201,8 +204,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return null;
}
- 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/Movies/TmdbSettings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
index 128258ab3..1ba8f9072 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
@@ -6,22 +6,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
internal class TmdbImageSettings
{
- public List<string> backdrop_sizes { get; set; }
+ public IReadOnlyList<string> backdrop_sizes { get; set; }
public string secure_base_url { get; set; }
- public List<string> poster_sizes { get; set; }
+ public IReadOnlyList<string> poster_sizes { get; set; }
- public List<string> profile_sizes { get; set; }
+ public IReadOnlyList<string> profile_sizes { get; set; }
public string GetImageUrl(string image)
{
return secure_base_url + image;
}
}
-
- internal class TmdbSettingsResult
- {
- public TmdbImageSettings images { get; set; }
- }
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index f8bc19395..31cfd8649 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers";
- internal static TmdbMovieProvider Current { get; private set; }
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
@@ -44,7 +44,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
private readonly ILibraryManager _libraryManager;
private readonly IApplicationHost _appHost;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ /// <summary>
+ /// The _TMDB settings task.
+ /// </summary>
+ private TmdbSettingsResult _tmdbSettings;
public TmdbMovieProvider(
IJsonSerializer jsonSerializer,
@@ -65,6 +68,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
Current = this;
}
+ 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)
{
return GetMovieSearchResults(searchInfo, cancellationToken);
@@ -131,13 +142,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return movieDb.GetMetadata(id, cancellationToken);
}
- public string Name => TmdbUtils.ProviderName;
-
- /// <summary>
- /// The _TMDB settings task.
- /// </summary>
- private TmdbSettingsResult _tmdbSettings;
-
/// <summary>
/// Gets the TMDB settings.
/// </summary>
@@ -155,7 +159,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var response = await GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ 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;
@@ -272,7 +276,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
languages.Add("en");
}
- return string.Join(",", languages);
+ return string.Join(',', languages);
}
public static string NormalizeLanguage(string language)
@@ -335,7 +339,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var mainResponse = await GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ using var mainResponse = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
if (mainResponse.StatusCode == HttpStatusCode.NotFound)
{
return null;
@@ -368,7 +372,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var langResponse = await GetMovieDbResponse(langRequestMessage).ConfigureAwait(false);
+ using var langResponse = await GetMovieDbResponse(langRequestMessage, cancellationToken).ConfigureAwait(false);
await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
var langResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
@@ -381,16 +385,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// <summary>
/// Gets the movie db response.
/// </summary>
- internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message)
+ /// <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);
+ return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message, cancellationToken);
}
/// <inheritdoc />
- public int Order => 1;
-
- /// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
index 2a6c6d035..36a4eef8a 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
@@ -198,7 +198,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ 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);
@@ -207,7 +207,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
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};
+ 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))
{
@@ -261,7 +266,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ 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);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
new file mode 100644
index 000000000..c7ba97438
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
@@ -0,0 +1,9 @@
+#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
index 73e49ba5b..b88ecce87 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
@@ -14,6 +14,8 @@ 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);
@@ -24,8 +26,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Music
return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>());
}
- public string Name => TmdbMovieProvider.Current.Name;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 6bf04b81a..777ebce49 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo>
{
- const string DataFileName = "info.json";
+ private const string DataFileName = "info.json";
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
@@ -39,20 +39,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<TmdbPersonProvider> _logger;
public TmdbPersonProvider(
IFileSystem fileSystem,
IServerConfigurationManager configurationManager,
IJsonSerializer jsonSerializer,
- IHttpClientFactory httpClientFactory,
- ILogger<TmdbPersonProvider> logger)
+ IHttpClientFactory httpClientFactory)
{
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
- _logger = logger;
Current = this;
}
@@ -75,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
- var images = (info.Images ?? new PersonImages()).Profiles ?? new List<Profile>();
+ IReadOnlyList<Profile> images = info.Images?.Profiles ?? Array.Empty<Profile>();
var result = new RemoteSearchResult
{
@@ -95,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (searchInfo.IsAutomated)
{
// Don't hammer moviedb searching by name
- return new List<RemoteSearchResult>();
+ return Array.Empty<RemoteSearchResult>();
}
var url = string.Format(
@@ -110,7 +107,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ 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)
@@ -243,7 +240,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ 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);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index eebecdac6..c56774f8e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -28,7 +28,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
: base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
- { }
+ {
+ }
+
+ public string Name => TmdbUtils.ProviderName;
+
+ // After TheTvDb
+ public int Order => 1;
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
@@ -43,7 +49,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var episode = (Controller.Entities.TV.Episode)item;
var series = episode.Series;
- var seriesId = series != null ? series.GetProviderId(MetadataProvider.Tmdb) : null;
+ var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
var list = new List<RemoteImageInfo>();
@@ -62,8 +68,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var language = item.GetPreferredMetadataLanguage();
- var response = await GetEpisodeInfo(seriesId, seasonNumber.Value, episodeNumber.Value,
- language, cancellationToken).ConfigureAwait(false);
+ var response = await GetEpisodeInfo(
+ seriesId,
+ seasonNumber.Value,
+ episodeNumber.Value,
+ language,
+ cancellationToken).ConfigureAwait(false);
var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
@@ -120,14 +130,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return GetResponse(url, cancellationToken);
}
- public string Name => TmdbUtils.ProviderName;
-
public bool Supports(BaseItem item)
{
return item is Controller.Entities.TV.Episode;
}
-
- // After TheTvDb
- public int Order => 1;
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 90e3cea93..a7e3a03fe 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -29,7 +29,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
: base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
- { }
+ {
+ }
+
+ // After TheTvDb
+ public int Order => 1;
+
+ public string Name => TmdbUtils.ProviderName;
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
@@ -41,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return list;
}
- var metadataResult = await GetMetadata(searchInfo, cancellationToken);
+ var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
if (metadataResult.HasMetadata)
{
@@ -109,7 +115,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
item.ParentIndexNumber = info.ParentIndexNumber;
item.IndexNumberEnd = info.IndexNumberEnd;
- if (response.External_Ids.Tvdb_Id > 0)
+ if (response.External_Ids != null && response.External_Ids.Tvdb_Id > 0)
{
item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
}
@@ -205,10 +211,5 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
return GetResponse(url, cancellationToken);
}
-
- // After TheTvDb
- public int Order => 1;
-
- public string Name => TmdbUtils.ProviderName;
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
index 30b7674e3..34d2424a3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
@@ -21,11 +21,11 @@ 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 ILocalizationManager _localization;
private readonly ILogger<TmdbEpisodeProviderBase> _logger;
protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
@@ -34,13 +34,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
_configurationManager = configurationManager;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
- _localization = localization;
_logger = loggerFactory.CreateLogger<TmdbEpisodeProviderBase>();
}
protected ILogger Logger => _logger;
- protected async Task<EpisodeResult> GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage,
+ protected async Task<EpisodeResult> GetEpisodeInfo(
+ string seriesTmdbId,
+ int season,
+ int episodeNumber,
+ string preferredMetadataLanguage,
CancellationToken cancellationToken)
{
await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken)
@@ -93,7 +96,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
- var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-episode-{1}-{2}.json",
+ var filename = string.Format(
+ CultureInfo.InvariantCulture,
+ "season-{0}-episode-{1}-{2}.json",
seasonNumber.ToString(CultureInfo.InvariantCulture),
episodeNumber.ToString(CultureInfo.InvariantCulture),
preferredLanguage);
@@ -113,7 +118,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
internal async Task<EpisodeResult> FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken)
{
- var url = string.Format(urlPattern, id, seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber, TmdbUtils.ApiKey);
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ urlPattern,
+ id,
+ seasonNumber.ToString(CultureInfo.InvariantCulture),
+ episodeNumber,
+ TmdbUtils.ApiKey);
if (!string.IsNullOrEmpty(language))
{
@@ -132,7 +143,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+ 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);
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index e7e2fd05b..dcc7f8700 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -112,9 +112,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
private async Task<List<Poster>> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken)
{
- await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, item.IndexNumber.GetValueOrDefault(), language, cancellationToken).ConfigureAwait(false);
+ var seasonNumber = item.IndexNumber.GetValueOrDefault();
+ await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, seasonNumber, language, cancellationToken).ConfigureAwait(false);
- var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
+ var path = TmdbSeasonProvider.Current.GetDataFilePath(tmdbId, seasonNumber, language);
if (!string.IsNullOrEmpty(path))
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 73ed13267..c9b257fcc 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -28,26 +28,32 @@ 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 ILocalizationManager _localization;
private readonly ILogger<TmdbSeasonProvider> _logger;
internal static TmdbSeasonProvider Current { get; private set; }
- public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger)
+ public TmdbSeasonProvider(
+ IHttpClientFactory httpClientFactory,
+ IServerConfigurationManager configurationManager,
+ IFileSystem fileSystem,
+ IJsonSerializer jsonSerializer,
+ ILogger<TmdbSeasonProvider> logger)
{
_httpClientFactory = httpClientFactory;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
- _localization = localization;
_jsonSerializer = jsonSerializer;
_logger = logger;
Current = this;
}
+ public string Name => TmdbUtils.ProviderName;
+
public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<Season>();
@@ -75,7 +81,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item.Overview = seasonInfo.Overview;
- if (seasonInfo.External_Ids.Tvdb_Id > 0)
+ 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));
}
@@ -116,8 +122,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return result;
}
- public string Name => TmdbUtils.ProviderName;
-
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
{
return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
@@ -128,7 +132,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
- private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage,
+ private async Task<SeasonResult> GetSeasonInfo(
+ string seriesTmdbId,
+ int season,
+ string preferredMetadataLanguage,
CancellationToken cancellationToken)
{
await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken)
@@ -181,7 +188,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
- var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-{1}.json",
+ var filename = string.Format(
+ CultureInfo.InvariantCulture,
+ "season-{0}-{1}.json",
seasonNumber.ToString(CultureInfo.InvariantCulture),
preferredLanguage);
@@ -200,7 +209,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
internal async Task<SeasonResult> FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken)
{
- var url = string.Format(GetTvInfo3, id, seasonNumber.ToString(CultureInfo.InvariantCulture), TmdbUtils.ApiKey);
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ GetTvInfo3,
+ id,
+ seasonNumber.ToString(CultureInfo.InvariantCulture),
+ TmdbUtils.ApiKey);
if (!string.IsNullOrEmpty(language))
{
@@ -219,7 +233,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage).ConfigureAwait(false);
+ 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);
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 125560175..179ceb825 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -12,7 +13,6 @@ using MediaBrowser.Controller.Entities.TV;
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;
@@ -25,19 +25,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IFileSystem _fileSystem;
- public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
+ public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
{
_jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
}
public string Name => ProviderName;
public static string ProviderName => TmdbUtils.ProviderName;
+ // After tvdb and fanart
+ public int Order => 2;
+
public bool Supports(BaseItem item)
{
return item is Series;
@@ -56,7 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
var list = new List<RemoteImageInfo>();
- var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false);
+ var results = await FetchImages(item, null, cancellationToken).ConfigureAwait(false);
if (results == null)
{
@@ -148,10 +149,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
/// </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,
+ private async Task<Images> FetchImages(
+ BaseItem item,
+ string language,
CancellationToken cancellationToken)
{
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
@@ -165,22 +167,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
- if (!string.IsNullOrEmpty(path))
+ if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
- var fileInfo = _fileSystem.GetFileInfo(path);
-
- if (fileInfo.Exists)
- {
- return jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
- }
+ return _jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
}
return null;
}
- // After tvdb and fanart
- public int Order => 2;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index aaba6ffc0..287ebca8c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -17,8 +17,6 @@ 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.Model.Providers;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
@@ -33,38 +31,35 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
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 IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<TmdbSeriesProvider> _logger;
- private readonly ILocalizationManager _localization;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
- internal static TmdbSeriesProvider Current { get; private set; }
-
public TmdbSeriesProvider(
IJsonSerializer jsonSerializer,
- IFileSystem fileSystem,
IServerConfigurationManager configurationManager,
ILogger<TmdbSeriesProvider> logger,
- ILocalizationManager localization,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager)
{
_jsonSerializer = jsonSerializer;
- _fileSystem = fileSystem;
_configurationManager = configurationManager;
_logger = logger;
- _localization = localization;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
Current = this;
}
+ internal static TmdbSeriesProvider Current { get; private set; }
+
public string Name => TmdbUtils.ProviderName;
+ // After TheTVDB
+ public int Order => 1;
+
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
@@ -92,7 +87,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
remoteResult.SetProviderId(MetadataProvider.Imdb, obj.External_Ids.Imdb_Id);
- if (obj.External_Ids.Tvdb_Id > 0)
+ if (obj.External_Ids != null && obj.External_Ids.Tvdb_Id > 0)
{
remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture));
}
@@ -129,8 +124,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult<Series>();
- result.QueriedById = true;
+ var result = new MetadataResult<Series>
+ {
+ QueriedById = true
+ };
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
@@ -206,9 +203,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
- var result = new MetadataResult<Series>();
- result.Item = new Series();
- result.ResultLanguage = seriesInfo.ResultLanguage;
+ var result = new MetadataResult<Series>
+ {
+ Item = new Series(),
+ ResultLanguage = seriesInfo.ResultLanguage
+ };
var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
@@ -405,7 +404,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
internal async Task<SeriesResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
{
- var url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey);
+ var url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey);
if (!string.IsNullOrEmpty(language))
{
@@ -421,7 +420,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage);
+ 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);
@@ -440,7 +439,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
_logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language);
- url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+ url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en";
if (!string.IsNullOrEmpty(language))
{
@@ -454,7 +453,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+ 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);
@@ -474,12 +473,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var path = GetDataFilePath(tmdbId, language);
- var fileInfo = _fileSystem.GetFileSystemInfo(path);
-
+ var fileInfo = new FileInfo(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)
+ if ((DateTime.UtcNow - fileInfo.LastWriteTimeUtc).TotalDays <= 2)
{
return Task.CompletedTask;
}
@@ -504,7 +502,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
private async Task<RemoteSearchResult> FindByExternalId(string id, string externalSource, CancellationToken cancellationToken)
{
- var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}",
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}",
id,
TmdbUtils.ApiKey,
externalSource);
@@ -515,7 +515,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
+ 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);
@@ -547,9 +547,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return null;
}
- // After TheTVDB
- public int Order => 1;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
index 25296387b..613dc17e3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
@@ -21,6 +21,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
_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);
@@ -31,10 +35,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
return TmdbMovieProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken);
}
- public string Name => TmdbMovieProvider.Current.Name;
-
- 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/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index 76dc7df7f..90e13f12f 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -33,6 +33,8 @@ namespace MediaBrowser.Providers.Studios
public string Name => "Emby Designs";
+ public int Order => 0;
+
public bool Supports(BaseItem item)
{
return item is Studio;
@@ -119,8 +121,6 @@ namespace MediaBrowser.Providers.Studios
return EnsureList(url, file, _fileSystem, cancellationToken);
}
- public int Order => 0;
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
@@ -161,12 +161,12 @@ namespace MediaBrowser.Providers.Studios
private string GetComparableName(string name)
{
- return name.Replace(" ", string.Empty)
- .Replace(".", string.Empty)
- .Replace("&", string.Empty)
- .Replace("!", string.Empty)
- .Replace(",", string.Empty)
- .Replace("/", string.Empty);
+ return name.Replace(" ", string.Empty, StringComparison.Ordinal)
+ .Replace(".", string.Empty, StringComparison.Ordinal)
+ .Replace("&", string.Empty, StringComparison.Ordinal)
+ .Replace("!", string.Empty, StringComparison.Ordinal)
+ .Replace(",", string.Empty, StringComparison.Ordinal)
+ .Replace("/", string.Empty, StringComparison.Ordinal);
}
public IEnumerable<string> GetAvailableImages(string file)
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 0f7cb3f8f..f25d3d5ee 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -303,7 +303,7 @@ namespace MediaBrowser.Providers.Subtitles
private ISubtitleProvider GetProvider(string id)
{
- return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name)));
+ return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name), StringComparison.Ordinal));
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs
index 0c09cdef6..905cbefd3 100644
--- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs
+++ b/MediaBrowser.Providers/TV/DummySeasonProvider.cs
@@ -124,7 +124,8 @@ namespace MediaBrowser.Providers.TV
/// <summary>
/// Adds the season.
/// </summary>
- public async Task<Season> AddSeason(Series series,
+ public async Task<Season> AddSeason(
+ Series series,
int? seasonNumber,
bool isVirtualItem,
CancellationToken cancellationToken)
@@ -211,11 +212,13 @@ namespace MediaBrowser.Providers.TV
{
_logger.LogInformation("Removing virtual season {0} {1}", series.Name, seasonToRemove.IndexNumber);
- _libraryManager.DeleteItem(seasonToRemove, new DeleteOptions
- {
- DeleteFileLocation = true
-
- }, false);
+ _libraryManager.DeleteItem(
+ seasonToRemove,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ false);
hasChanges = true;
}
diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
index 09850beb0..c833b1227 100644
--- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
+++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
@@ -48,18 +48,25 @@ namespace MediaBrowser.Providers.TV
public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken)
{
- var tvdbId = series.GetProviderId(MetadataProvider.Tvdb);
- if (string.IsNullOrEmpty(tvdbId))
+ var tvdbIdString = series.GetProviderId(MetadataProvider.Tvdb);
+ if (string.IsNullOrEmpty(tvdbIdString))
{
return false;
}
- var episodes = await _tvdbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken);
+ var episodes = await _tvdbClientManager.GetAllEpisodesAsync(
+ int.Parse(tvdbIdString, CultureInfo.InvariantCulture),
+ series.GetPreferredMetadataLanguage(),
+ cancellationToken).ConfigureAwait(false);
var episodeLookup = episodes
.Select(i =>
{
- DateTime.TryParse(i.FirstAired, out var firstAired);
+ 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);
@@ -159,7 +166,7 @@ namespace MediaBrowser.Providers.TV
var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays);
- if (airDate < now && addMissingEpisodes || airDate > now)
+ 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);
@@ -232,10 +239,13 @@ namespace MediaBrowser.Providers.TV
foreach (var episodeToRemove in episodesToRemove)
{
- _libraryManager.DeleteItem(episodeToRemove, new DeleteOptions
- {
- DeleteFileLocation = true
- }, false);
+ _libraryManager.DeleteItem(
+ episodeToRemove,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ false);
hasChanges = true;
}
@@ -246,7 +256,7 @@ namespace MediaBrowser.Providers.TV
/// <summary>
/// Removes the obsolete or missing seasons.
/// </summary>
- /// <param name="allRecursiveChildren"></param>
+ /// <param name="allRecursiveChildren">All recursive children.</param>
/// <param name="episodeLookup">The episode lookup.</param>
/// <returns><see cref="bool" />.</returns>
private bool RemoveObsoleteOrMissingSeasons(
@@ -297,10 +307,13 @@ namespace MediaBrowser.Providers.TV
foreach (var seasonToRemove in seasonsToRemove)
{
- _libraryManager.DeleteItem(seasonToRemove, new DeleteOptions
- {
- DeleteFileLocation = true
- }, false);
+ _libraryManager.DeleteItem(
+ seasonToRemove,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ false);
hasChanges = true;
}
@@ -354,7 +367,10 @@ namespace MediaBrowser.Providers.TV
/// <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)
+ 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;
diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
index 5431de623..4e59f78bc 100644
--- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
@@ -28,6 +28,9 @@ namespace MediaBrowser.Providers.TV
}
/// <inheritdoc />
+ protected override bool EnableUpdatingPremiereDateFromChildren => true;
+
+ /// <inheritdoc />
protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType currentUpdateType)
{
var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
@@ -68,9 +71,6 @@ namespace MediaBrowser.Providers.TV
}
/// <inheritdoc />
- protected override bool EnableUpdatingPremiereDateFromChildren => true;
-
- /// <inheritdoc />
protected override IList<BaseItem> GetChildrenForMetadataUpdates(Season item)
=> item.GetEpisodes();
diff --git a/MediaBrowser.Providers/TV/TvExternalIds.cs b/MediaBrowser.Providers/TV/TvExternalIds.cs
deleted file mode 100644
index a6040edd1..000000000
--- a/MediaBrowser.Providers/TV/TvExternalIds.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.TheTvdb;
-
-namespace MediaBrowser.Providers.TV
-{
- public class Zap2ItExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "Zap2It";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.Zap2It.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => null;
-
- /// <inheritdoc />
- public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Series;
- }
-
- public class TvdbExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "TheTVDB";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.Tvdb.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => null;
-
- /// <inheritdoc />
- public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Series;
- }
-
- public class TvdbSeasonExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "TheTVDB";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.Tvdb.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Season;
-
- /// <inheritdoc />
- public string UrlFormatString => null;
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Season;
- }
-
- public class TvdbEpisodeExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "TheTVDB";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.Tvdb.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
-
- /// <inheritdoc />
- public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Episode;
- }
-}
diff --git a/MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs b/MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs
new file mode 100644
index 000000000..40c5f2d78
--- /dev/null
+++ b/MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.TheTvdb;
+
+namespace MediaBrowser.Providers.TV
+{
+ public class TvdbEpisodeExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "TheTVDB";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.Tvdb.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
+
+ /// <inheritdoc />
+ public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Episode;
+ }
+}
diff --git a/MediaBrowser.Providers/TV/TvdbExternalId.cs b/MediaBrowser.Providers/TV/TvdbExternalId.cs
new file mode 100644
index 000000000..4c54de9f8
--- /dev/null
+++ b/MediaBrowser.Providers/TV/TvdbExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.TheTvdb;
+
+namespace MediaBrowser.Providers.TV
+{
+ public class TvdbExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "TheTVDB";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.Tvdb.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => null;
+
+ /// <inheritdoc />
+ public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Series;
+ }
+}
diff --git a/MediaBrowser.Providers/TV/TvdbSeasonExternalId.cs b/MediaBrowser.Providers/TV/TvdbSeasonExternalId.cs
new file mode 100644
index 000000000..807ebb3ee
--- /dev/null
+++ b/MediaBrowser.Providers/TV/TvdbSeasonExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.TheTvdb;
+
+namespace MediaBrowser.Providers.TV
+{
+ public class TvdbSeasonExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "TheTVDB";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.Tvdb.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Season;
+
+ /// <inheritdoc />
+ public string UrlFormatString => null;
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Season;
+ }
+}
diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
new file mode 100644
index 000000000..c9f314af9
--- /dev/null
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.TheTvdb;
+
+namespace MediaBrowser.Providers.TV
+{
+ public class Zap2ItExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "Zap2It";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.Zap2It.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => null;
+
+ /// <inheritdoc />
+ public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Series;
+ }
+}
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index fd45dca2a..25402aee1 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -208,6 +208,5 @@ Global
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
- {7C93C84F-105C-48E5-A878-406FA0A5B296} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index 55d6917ae..435e709b3 100644
--- a/README.md
+++ b/README.md
@@ -53,18 +53,19 @@ Jellyfin is a Free Software Media System that puts you in control of managing an
For further details, please see [our documentation page](https://docs.jellyfin.org/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://docs.jellyfin.org/general/getting-help.html). For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html).
<strong>Want to get started?</strong><br/>
-Choose from <a href="https://docs.jellyfin.org/general/administration/installing.html">Prebuilt Packages</a> or <a href="https://docs.jellyfin.org/general/administration/building.html">Build from Source</a>, then see our <a href="https://docs.jellyfin.org/general/quick-start.html">quick start guide</a>.<br/>
+Check out our <a href="https://jellyfin.org/downloads">downloads page</a> or our <a href="https://docs.jellyfin.org/general/administration/installing.html">installation guide</a>, then see our <a href="https://docs.jellyfin.org/general/quick-start.html">quick start guide</a>. You can also <a href="https://docs.jellyfin.org/general/administration/building.html">build from source</a>.<br/>
<strong>Something not working right?</strong><br/>
Open an <a href="https://docs.jellyfin.org/general/contributing/issues.html">Issue</a> on GitHub.<br/>
<strong>Want to contribute?</strong><br/>
-Check out <a href="https://docs.jellyfin.org/general/contributing/index.html">our documentation for guidelines</a>.<br/>
+Check out our <a href="https://jellyfin.org/contribute">contributing choose-your-own-adventure</a> to see where you can help, then see our <a href="https://docs.jellyfin.org/general/contributing/index.html">contributing guide</a> and our <a href="https://jellyfin.org/docs/general/community-standards">community standards</a>.<br/>
<strong>New idea or improvement?</strong><br/>
Check out our <a href="https://features.jellyfin.org/?view=most-wanted">feature request hub</a>.<br/>
-Most of the translations can be found in the web client but we have several other clients that have missing strings. Translations can be improved very easily from our <a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-core">Weblate</a> instance. Look through the following graphic to see if your native language could use some work!
+<strong>Don't see Jellyfin in your language?</strong><br/>
+Check out our <a href="https://translate.jellyfin.org">Weblate instance</a> to help translate Jellyfin and its subprojects.<br/>
<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget">
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-web/multi-auto.svg" alt="Detailed Translation Status"/>
@@ -124,7 +125,7 @@ To run the project with Visual Studio Code you will first need to open the repos
Second, you need to [install the recommended extensions for the workspace](https://code.visualstudio.com/docs/editor/extension-gallery#_recommended-extensions). Note that extension recommendations are classified as either "Workspace Recommendations" or "Other Recommendations", but only the "Workspace Recommendations" are required.
-After the required extensions are installed, you can can run the server by pressing `F5`.
+After the required extensions are installed, you can run the server by pressing `F5`.
#### Running From The Command Line
diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs
index 66a0c5ec4..745ec359c 100644
--- a/RSSDP/DisposableManagedObjectBase.cs
+++ b/RSSDP/DisposableManagedObjectBase.cs
@@ -43,13 +43,13 @@ namespace Rssdp.Infrastructure
{
var builder = new StringBuilder();
- const string argFormat = "{0}: {1}\r\n";
+ const string ArgFormat = "{0}: {1}\r\n";
builder.AppendFormat("{0}\r\n", header);
foreach (var pair in values)
{
- builder.AppendFormat(argFormat, pair.Key, pair.Value);
+ builder.AppendFormat(ArgFormat, pair.Key, pair.Value);
}
builder.Append("\r\n");
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index 1ac5f76d6..7202c5883 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 68381e7bf..e9f30213f 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 ce1b100c1..91a8a6e7a 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 b4a3c1b76..828d5c2cf 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 7912e018e..0b2a0fe5f 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 949f1ef8f..7d5de230f 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 9518d8493..9c63f43df 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 0174f2f2a..51612dd44 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 0e02240c8..4ed7f8687 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& 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 d1f2f9e48..5671cc598 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/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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 \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 37b573e50..bfb2b3be2 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -84,6 +84,10 @@ EOF
%{_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/*
# 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/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index bcba3a203..e3a7a5428 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -16,8 +16,8 @@
<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.7" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.7" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.8" />
<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" />
diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs
index 77f1640fa..bd3d35687 100644
--- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs
@@ -47,8 +47,7 @@ namespace Jellyfin.Api.Tests
// Specify the startup command line options
var commandLineOpts = new StartupOptions
{
- NoWebClient = true,
- NoAutoRunWebApp = true
+ NoWebClient = true
};
// Use a temporary directory for the application paths
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs
new file mode 100644
index 000000000..a214bc57c
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs
@@ -0,0 +1,30 @@
+using Emby.Naming.AudioBook;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+ public class AudioBookFileInfoTests
+ {
+ [Fact]
+ public void CompareTo_Same_Success()
+ {
+ var info = new AudioBookFileInfo();
+ Assert.Equal(0, info.CompareTo(info));
+ }
+
+ [Fact]
+ public void CompareTo_Null_Success()
+ {
+ var info = new AudioBookFileInfo();
+ Assert.Equal(1, info.CompareTo(null));
+ }
+
+ [Fact]
+ public void CompareTo_Empty_Success()
+ {
+ var info1 = new AudioBookFileInfo();
+ var info2 = new AudioBookFileInfo();
+ Assert.Equal(0, info1.CompareTo(info2));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
new file mode 100644
index 000000000..1084e20bd
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
@@ -0,0 +1,90 @@
+using System.Linq;
+using Emby.Naming.AudioBook;
+using Emby.Naming.Common;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+ public class AudioBookListResolverTests
+ {
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ [Fact]
+ public void TestStackAndExtras()
+ {
+ // No stacking here because there is no part/disc/etc
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows/Part 1.mp3",
+ "Harry Potter and the Deathly Hallows/Part 2.mp3",
+ "Harry Potter and the Deathly Hallows/book.nfo",
+
+ "Batman/Chapter 1.mp3",
+ "Batman/Chapter 2.mp3",
+ "Batman/Chapter 3.mp3",
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ })).ToList();
+
+ Assert.Equal(2, result[0].Files.Count);
+ // Assert.Empty(result[0].Extras); FIXME: AudioBookListResolver should resolve extra files properly
+ Assert.Equal("Harry Potter and the Deathly Hallows", result[0].Name);
+
+ Assert.Equal(3, result[1].Files.Count);
+ Assert.Empty(result[1].Extras);
+ Assert.Equal("Batman", result[1].Name);
+ }
+
+ [Fact]
+ public void TestWithMetadata()
+ {
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows/Chapter 1.ogg",
+ "Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows.nfo"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }));
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void TestWithExtra()
+ {
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows/Chapter 1.mp3",
+ "Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows trailer.mp3"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ })).ToList();
+
+ Assert.Single(result);
+ }
+
+ private AudioBookListResolver GetResolver()
+ {
+ return new AudioBookListResolver(_namingOptions);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
new file mode 100644
index 000000000..83d44721c
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using Emby.Naming.AudioBook;
+using Emby.Naming.Common;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+ public class AudioBookResolverTests
+ {
+ private readonly NamingOptions _namingOptions = new NamingOptions();
+
+ public static IEnumerable<object[]> GetResolveFileTestData()
+ {
+ yield return new object[]
+ {
+ new AudioBookFileInfo()
+ {
+ Path = @"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
+ Container = "mp3",
+ }
+ };
+ yield return new object[]
+ {
+ new AudioBookFileInfo()
+ {
+ Path = @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
+ Container = "ogg",
+ ChapterNumber = 1
+ }
+ };
+ yield return new object[]
+ {
+ new AudioBookFileInfo()
+ {
+ Path = @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
+ Container = "mp3",
+ ChapterNumber = 2,
+ PartNumber = 3
+ }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(GetResolveFileTestData))]
+ public void ResolveFile_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);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index a2722a175..8dfb8f859 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -44,14 +44,14 @@ namespace Jellyfin.Naming.Tests.Video
}
[Theory]
- [InlineData(ExtraType.BehindTheScenes, "behind the scenes" )]
- [InlineData(ExtraType.DeletedScene, "deleted scenes" )]
- [InlineData(ExtraType.Interview, "interviews" )]
- [InlineData(ExtraType.Scene, "scenes" )]
- [InlineData(ExtraType.Sample, "samples" )]
- [InlineData(ExtraType.Clip, "shorts" )]
- [InlineData(ExtraType.Clip, "featurettes" )]
- [InlineData(ExtraType.Unknown, "extras" )]
+ [InlineData(ExtraType.BehindTheScenes, "behind the scenes")]
+ [InlineData(ExtraType.DeletedScene, "deleted scenes")]
+ [InlineData(ExtraType.Interview, "interviews")]
+ [InlineData(ExtraType.Scene, "scenes")]
+ [InlineData(ExtraType.Sample, "samples")]
+ [InlineData(ExtraType.Clip, "shorts")]
+ [InlineData(ExtraType.Clip, "featurettes")]
+ [InlineData(ExtraType.Unknown, "extras")]
public void TestDirectories(ExtraType type, string dirName)
{
Test(dirName + "/300.mp4", type, _videoOptions);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index c771f5f4a..6d768af89 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -10,6 +10,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
+ [InlineData("Superman: Red Son", "something", null)]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));