aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-abi.yml2
-rw-r--r--.ci/azure-pipelines-api-client.yml3
-rw-r--r--.ci/azure-pipelines-main.yml2
-rw-r--r--.ci/azure-pipelines-package.yml6
-rw-r--r--.ci/azure-pipelines-test.yml8
-rw-r--r--.ci/azure-pipelines.yml2
-rw-r--r--.vscode/launch.json4
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Dockerfile4
-rw-r--r--Dockerfile.arm4
-rw-r--r--Dockerfile.arm644
-rw-r--r--DvdLib/DvdLib.csproj2
-rw-r--r--Emby.Dlna/Common/Argument.cs20
-rw-r--r--Emby.Dlna/Common/DeviceIcon.cs32
-rw-r--r--Emby.Dlna/Common/DeviceService.cs33
-rw-r--r--Emby.Dlna/Common/ServiceAction.cs21
-rw-r--r--Emby.Dlna/Common/StateVariable.cs31
-rw-r--r--Emby.Dlna/Configuration/DlnaOptions.cs69
-rw-r--r--Emby.Dlna/ConnectionManager/ConnectionManagerService.cs12
-rw-r--r--Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs145
-rw-r--r--Emby.Dlna/ConnectionManager/ControlHandler.cs13
-rw-r--r--Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs37
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectoryService.cs39
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs229
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs541
-rw-r--r--Emby.Dlna/ContentDirectory/ServerItem.cs13
-rw-r--r--Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs51
-rw-r--r--Emby.Dlna/ContentDirectory/StubType.cs3
-rw-r--r--Emby.Dlna/DlnaManager.cs4
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj2
-rw-r--r--Emby.Dlna/PlayTo/Device.cs8
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs5
-rw-r--r--Emby.Dlna/PlayTo/SsdpHttpClient.cs4
-rw-r--r--Emby.Drawing/Emby.Drawing.csproj2
-rw-r--r--Emby.Naming/Audio/AlbumParser.cs15
-rw-r--r--Emby.Naming/Audio/AudioFileParser.cs12
-rw-r--r--Emby.Naming/AudioBook/AudioBookFileInfo.cs23
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParser.cs17
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs14
-rw-r--r--Emby.Naming/AudioBook/AudioBookInfo.cs15
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs129
-rw-r--r--Emby.Naming/AudioBook/AudioBookNameParser.cs67
-rw-r--r--Emby.Naming/AudioBook/AudioBookNameParserResult.cs18
-rw-r--r--Emby.Naming/AudioBook/AudioBookResolver.cs39
-rw-r--r--Emby.Naming/Common/EpisodeExpression.cs42
-rw-r--r--Emby.Naming/Common/MediaType.cs5
-rw-r--r--Emby.Naming/Common/NamingOptions.cs648
-rw-r--r--Emby.Naming/Emby.Naming.csproj5
-rw-r--r--Emby.Naming/Subtitles/SubtitleInfo.cs20
-rw-r--r--Emby.Naming/Subtitles/SubtitleParser.cs29
-rw-r--r--Emby.Naming/TV/EpisodeInfo.cs45
-rw-r--r--Emby.Naming/TV/EpisodePathParser.cs35
-rw-r--r--Emby.Naming/TV/EpisodePathParserResult.cs37
-rw-r--r--Emby.Naming/TV/EpisodeResolver.cs25
-rw-r--r--Emby.Naming/TV/SeasonPathParser.cs24
-rw-r--r--Emby.Naming/TV/SeasonPathParserResult.cs9
-rw-r--r--Emby.Naming/Video/CleanDateTimeParser.cs9
-rw-r--r--Emby.Naming/Video/CleanDateTimeResult.cs19
-rw-r--r--Emby.Naming/Video/CleanStringParser.cs10
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs18
-rw-r--r--Emby.Naming/Video/ExtraResult.cs7
-rw-r--r--Emby.Naming/Video/ExtraRule.cs17
-rw-r--r--Emby.Naming/Video/ExtraRuleType.cs7
-rw-r--r--Emby.Naming/Video/FileStack.cs25
-rw-r--r--Emby.Naming/Video/FlagParser.cs26
-rw-r--r--Emby.Naming/Video/Format3DParser.cs30
-rw-r--r--Emby.Naming/Video/Format3DResult.cs10
-rw-r--r--Emby.Naming/Video/Format3DRule.cs22
-rw-r--r--Emby.Naming/Video/StackResolver.cs66
-rw-r--r--Emby.Naming/Video/StubResolver.cs15
-rw-r--r--Emby.Naming/Video/StubResult.cs19
-rw-r--r--Emby.Naming/Video/StubTypeRule.cs16
-rw-r--r--Emby.Naming/Video/VideoFileInfo.cs40
-rw-r--r--Emby.Naming/Video/VideoInfo.cs4
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs35
-rw-r--r--Emby.Naming/Video/VideoResolver.cs66
-rw-r--r--Emby.Notifications/Emby.Notifications.csproj2
-rw-r--r--Emby.Notifications/NotificationEntryPoint.cs10
-rw-r--r--Emby.Photos/Emby.Photos.csproj2
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs6
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs7
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs2
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs3
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs14
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs8
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs71
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj16
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs14
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs13
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs26
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json5
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs6
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs3
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs24
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs5
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs2
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs12
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs112
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs6
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs8
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs17
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs259
-rw-r--r--Jellyfin.Api/Controllers/EnvironmentController.cs8
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs41
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs12
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs26
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs12
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs187
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs19
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs35
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs14
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs5
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs8
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs13
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs18
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs15
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs8
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs13
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs90
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs15
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs8
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs179
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs9
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs18
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs9
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs205
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs10
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs98
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs62
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileCopier.cs6
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs43
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs57
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs64
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj8
-rw-r--r--Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs90
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs9
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs4
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs2
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs6
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs2
-rw-r--r--Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs6
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs14
-rw-r--r--Jellyfin.Data/Entities/Libraries/CollectionItem.cs4
-rw-r--r--Jellyfin.Data/Entities/Libraries/ItemMetadata.cs2
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj6
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj2
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs11
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj8
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs3
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs7
-rw-r--r--Jellyfin.Server/CoreAppHost.cs2
-rw-r--r--Jellyfin.Server/Filters/FileResponseFilter.cs8
-rw-r--r--Jellyfin.Server/Formatters/CssOutputFormatter.cs3
-rw-r--r--Jellyfin.Server/Formatters/XmlOutputFormatter.cs3
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj10
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs3
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonDateTimeIso8601Converter.cs24
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs75
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs28
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs1
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj10
-rw-r--r--MediaBrowser.Common/Net/DefaultHttpClientHandler.cs3
-rw-r--r--MediaBrowser.Common/Net/INetworkManager.cs4
-rw-r--r--MediaBrowser.Common/Plugins/BasePlugin.cs21
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs86
-rw-r--r--MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs29
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs56
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs8
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs6
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs4
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs544
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs12
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj2
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs34
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj4
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs105
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs4
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs3
-rw-r--r--MediaBrowser.Model/Dlna/DeviceIdentification.cs23
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs217
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionNormalizer.cs12
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs98
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs13
-rw-r--r--MediaBrowser.Model/Dlna/XmlAttribute.cs9
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs5
-rw-r--r--MediaBrowser.Model/Entities/ProviderIdsExtensions.cs2
-rw-r--r--MediaBrowser.Model/Extensions/StringHelper.cs6
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj8
-rw-r--r--MediaBrowser.Model/Net/HttpException.cs42
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs8
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs8
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs9
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs23
-rw-r--r--MediaBrowser.Providers/Manager/ProviderUtils.cs4
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj10
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs29
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs289
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs130
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs262
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs113
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs155
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs153
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs419
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs39
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs2
-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.cs1
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj2
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs10
-rw-r--r--README.md4
-rw-r--r--RSSDP/DisposableManagedObjectBase.cs6
-rw-r--r--RSSDP/HttpParserBase.cs2
-rw-r--r--RSSDP/ISsdpDeviceLocator.cs4
-rw-r--r--RSSDP/RSSDP.csproj2
-rw-r--r--RSSDP/SsdpDevicePublisher.cs4
-rw-r--r--RSSDP/SsdpRootDevice.cs4
-rw-r--r--benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj2
-rw-r--r--debian/control2
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.debian.amd644
-rw-r--r--deployment/Dockerfile.debian.arm644
-rw-r--r--deployment/Dockerfile.debian.armhf4
-rw-r--r--deployment/Dockerfile.docker.amd644
-rw-r--r--deployment/Dockerfile.docker.arm644
-rw-r--r--deployment/Dockerfile.docker.armhf4
-rw-r--r--deployment/Dockerfile.fedora.amd646
-rw-r--r--deployment/Dockerfile.linux.amd644
-rw-r--r--deployment/Dockerfile.macos4
-rw-r--r--deployment/Dockerfile.portable4
-rw-r--r--deployment/Dockerfile.ubuntu.amd644
-rw-r--r--deployment/Dockerfile.ubuntu.arm644
-rw-r--r--deployment/Dockerfile.ubuntu.armhf4
-rw-r--r--deployment/Dockerfile.windows.amd644
-rwxr-xr-xdeployment/build.debian.amd644
-rwxr-xr-xdeployment/build.debian.arm644
-rwxr-xr-xdeployment/build.debian.armhf4
-rwxr-xr-xdeployment/build.ubuntu.amd644
-rwxr-xr-xdeployment/build.ubuntu.arm644
-rwxr-xr-xdeployment/build.ubuntu.armhf4
-rw-r--r--fedora/jellyfin.spec3
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs8
-rw-r--r--tests/Jellyfin.Api.Tests/BrandingServiceTests.cs4
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj8
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs32
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs226
-rw-r--r--tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs2
-rw-r--r--tests/Jellyfin.Api.Tests/TestHelpers.cs2
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj2
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj2
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs10
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs189
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs47
-rw-r--r--tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs36
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs10
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs103
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs38
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs27
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs21
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs79
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StubTests.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs29
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs235
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj4
312 files changed, 5698 insertions, 4263 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index b558d2a6f..14df7e7c8 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
- default: 3.1.100
+ default: 5.0.100
jobs:
- job: CompatibilityCheck
diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml
index 03102121f..1c447fd97 100644
--- a/.ci/azure-pipelines-api-client.yml
+++ b/.ci/azure-pipelines-api-client.yml
@@ -9,6 +9,7 @@
jobs:
- job: GenerateApiClients
displayName: 'Generate Api Clients'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
dependsOn: Test
pool:
@@ -37,7 +38,6 @@ jobs:
## Generate npm api client
- task: CmdLine@2
displayName: 'Build stable typescript axios client'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
@@ -51,7 +51,6 @@ jobs:
## Publish npm packages
- task: Npm@1
displayName: 'Publish stable typescript axios client'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: publish
publishRegistry: useExternalRegistry
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
index 7617f0f5a..95dd3ccac 100644
--- a/.ci/azure-pipelines-main.yml
+++ b/.ci/azure-pipelines-main.yml
@@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
- DotNetSdkVersion: 3.1.100
+ DotNetSdkVersion: 5.0.100
jobs:
- job: Build
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 0dc604a79..d478516b8 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -188,6 +188,12 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
+ - task: UseDotNet@2
+ displayName: 'Use .NET 5.0 sdk'
+ inputs:
+ packageType: 'sdk'
+ version: '5.0.x'
+
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 6a36698b5..36152c82a 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
- default: 3.1.100
+ default: 5.0.100
jobs:
- job: Test
@@ -30,11 +30,11 @@ jobs:
# This is required for the SonarCloud analyzer
- task: UseDotNet@2
- displayName: "Install .NET Core SDK 2.1"
+ displayName: "Install .NET SDK 5.x"
condition: eq(variables['ImageName'], 'ubuntu-latest')
inputs:
packageType: sdk
- version: '2.1.805'
+ version: '5.x'
- task: UseDotNet@2
displayName: "Update DotNet"
@@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
- targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
+ targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 5b5a17dea..ec4c25435 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion
- value: 3.1.100
+ value: 5.0.100
pr:
autoCancel: true
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 05f60cfa6..e55ea2248 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 7b4772730..a97a4c741 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -104,6 +104,7 @@
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
+ - [ssenart] (https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000)
diff --git a/Dockerfile b/Dockerfile
index 69af9b77b..963027b49 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
@@ -8,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& yarn install \
&& mv dist /dist
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index efeed25df..e0eaca0ed 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 1f2c2ec36..db7de935c 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
FROM node:alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index 64d041cb0..7bbd9acf8 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -10,7 +10,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/Emby.Dlna/Common/Argument.cs b/Emby.Dlna/Common/Argument.cs
index f375e6049..e4e9c55e0 100644
--- a/Emby.Dlna/Common/Argument.cs
+++ b/Emby.Dlna/Common/Argument.cs
@@ -1,13 +1,23 @@
-#pragma warning disable CS1591
-
namespace Emby.Dlna.Common
{
+ /// <summary>
+ /// DLNA Query parameter type, used when querying DLNA devices via SOAP.
+ /// </summary>
public class Argument
{
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets name of the DLNA argument.
+ /// </summary>
+ public string Name { get; set; } = string.Empty;
- public string Direction { get; set; }
+ /// <summary>
+ /// Gets or sets the direction of the parameter.
+ /// </summary>
+ public string Direction { get; set; } = string.Empty;
- public string RelatedStateVariable { get; set; }
+ /// <summary>
+ /// Gets or sets the related DLNA state variable for this argument.
+ /// </summary>
+ public string RelatedStateVariable { get; set; } = string.Empty;
}
}
diff --git a/Emby.Dlna/Common/DeviceIcon.cs b/Emby.Dlna/Common/DeviceIcon.cs
index c3f7fa8aa..f9fd1dcec 100644
--- a/Emby.Dlna/Common/DeviceIcon.cs
+++ b/Emby.Dlna/Common/DeviceIcon.cs
@@ -1,29 +1,41 @@
-#pragma warning disable CS1591
-
using System.Globalization;
namespace Emby.Dlna.Common
{
+ /// <summary>
+ /// Defines the <see cref="DeviceIcon" />.
+ /// </summary>
public class DeviceIcon
{
- public string Url { get; set; }
+ /// <summary>
+ /// Gets or sets the Url.
+ /// </summary>
+ public string Url { get; set; } = string.Empty;
- public string MimeType { get; set; }
+ /// <summary>
+ /// Gets or sets the MimeType.
+ /// </summary>
+ public string MimeType { get; set; } = string.Empty;
+ /// <summary>
+ /// Gets or sets the Width.
+ /// </summary>
public int Width { get; set; }
+ /// <summary>
+ /// Gets or sets the Height.
+ /// </summary>
public int Height { get; set; }
- public string Depth { get; set; }
+ /// <summary>
+ /// Gets or sets the Depth.
+ /// </summary>
+ public string Depth { get; set; } = string.Empty;
/// <inheritdoc />
public override string ToString()
{
- return string.Format(
- CultureInfo.InvariantCulture,
- "{0}x{1}",
- Height,
- Width);
+ return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width);
}
}
}
diff --git a/Emby.Dlna/Common/DeviceService.cs b/Emby.Dlna/Common/DeviceService.cs
index 44c0a0412..c1369558e 100644
--- a/Emby.Dlna/Common/DeviceService.cs
+++ b/Emby.Dlna/Common/DeviceService.cs
@@ -1,21 +1,36 @@
-#pragma warning disable CS1591
-
namespace Emby.Dlna.Common
{
+ /// <summary>
+ /// Defines the <see cref="DeviceService" />.
+ /// </summary>
public class DeviceService
{
- public string ServiceType { get; set; }
+ /// <summary>
+ /// Gets or sets the Service Type.
+ /// </summary>
+ public string ServiceType { get; set; } = string.Empty;
- public string ServiceId { get; set; }
+ /// <summary>
+ /// Gets or sets the Service Id.
+ /// </summary>
+ public string ServiceId { get; set; } = string.Empty;
- public string ScpdUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the Scpd Url.
+ /// </summary>
+ public string ScpdUrl { get; set; } = string.Empty;
- public string ControlUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the Control Url.
+ /// </summary>
+ public string ControlUrl { get; set; } = string.Empty;
- public string EventSubUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the EventSubUrl.
+ /// </summary>
+ public string EventSubUrl { get; set; } = string.Empty;
/// <inheritdoc />
- public override string ToString()
- => ServiceId;
+ public override string ToString() => ServiceId;
}
}
diff --git a/Emby.Dlna/Common/ServiceAction.cs b/Emby.Dlna/Common/ServiceAction.cs
index d458d7f3f..02b81a0aa 100644
--- a/Emby.Dlna/Common/ServiceAction.cs
+++ b/Emby.Dlna/Common/ServiceAction.cs
@@ -1,24 +1,31 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
namespace Emby.Dlna.Common
{
+ /// <summary>
+ /// Defines the <see cref="ServiceAction" />.
+ /// </summary>
public class ServiceAction
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ServiceAction"/> class.
+ /// </summary>
public ServiceAction()
{
ArgumentList = new List<Argument>();
}
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the action.
+ /// </summary>
+ public string Name { get; set; } = string.Empty;
+ /// <summary>
+ /// Gets the ArgumentList.
+ /// </summary>
public List<Argument> ArgumentList { get; }
/// <inheritdoc />
- public override string ToString()
- {
- return Name;
- }
+ public override string ToString() => Name;
}
}
diff --git a/Emby.Dlna/Common/StateVariable.cs b/Emby.Dlna/Common/StateVariable.cs
index 6daf7ab6b..fd733e085 100644
--- a/Emby.Dlna/Common/StateVariable.cs
+++ b/Emby.Dlna/Common/StateVariable.cs
@@ -1,27 +1,34 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
namespace Emby.Dlna.Common
{
+ /// <summary>
+ /// Defines the <see cref="StateVariable" />.
+ /// </summary>
public class StateVariable
{
- public StateVariable()
- {
- AllowedValues = Array.Empty<string>();
- }
-
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the state variable.
+ /// </summary>
+ public string Name { get; set; } = string.Empty;
- public string DataType { get; set; }
+ /// <summary>
+ /// Gets or sets the data type of the state variable.
+ /// </summary>
+ public string DataType { get; set; } = string.Empty;
+ /// <summary>
+ /// Gets or sets a value indicating whether it sends events.
+ /// </summary>
public bool SendsEvents { get; set; }
- public IReadOnlyList<string> AllowedValues { get; set; }
+ /// <summary>
+ /// Gets or sets the allowed values range.
+ /// </summary>
+ public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>();
/// <inheritdoc />
- public override string ToString()
- => Name;
+ public override string ToString() => Name;
}
}
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
index 6dd9a445a..e63a85860 100644
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ b/Emby.Dlna/Configuration/DlnaOptions.cs
@@ -2,8 +2,14 @@
namespace Emby.Dlna.Configuration
{
+ /// <summary>
+ /// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
+ /// </summary>
public class DlnaOptions
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DlnaOptions"/> class.
+ /// </summary>
public DlnaOptions()
{
EnablePlayTo = true;
@@ -11,23 +17,76 @@ namespace Emby.Dlna.Configuration
BlastAliveMessages = true;
SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60;
- BlastAliveMessageIntervalSeconds = 1800;
+ AliveMessageIntervalSeconds = 1800;
}
+ /// <summary>
+ /// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem.
+ /// </summary>
public bool EnablePlayTo { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem.
+ /// </summary>
public bool EnableServer { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether detailed dlna server logs are sent to the console/log.
+ /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
+ /// </summary>
public bool EnableDebugLog { get; set; }
- public bool BlastAliveMessages { get; set; }
-
- public bool SendOnlyMatchedHost { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log.
+ /// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work.
+ /// </summary>
+ public bool EnablePlayToTracing { get; set; }
+ /// <summary>
+ /// Gets or sets the ssdp client discovery interval time (in seconds).
+ /// This is the time after which the server will send a ssdp search request.
+ /// </summary>
public int ClientDiscoveryIntervalSeconds { get; set; }
- public int BlastAliveMessageIntervalSeconds { get; set; }
+ /// <summary>
+ /// Gets or sets the frequency at which ssdp alive notifications are transmitted.
+ /// </summary>
+ public int AliveMessageIntervalSeconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the frequency at which ssdp alive notifications are transmitted. MIGRATING - TO BE REMOVED ONCE WEB HAS BEEN ALTERED.
+ /// </summary>
+ public int BlastAliveMessageIntervalSeconds
+ {
+ get
+ {
+ return AliveMessageIntervalSeconds;
+ }
+
+ set
+ {
+ AliveMessageIntervalSeconds = value;
+ }
+ }
+ /// <summary>
+ /// Gets or sets the default user account that the dlna server uses.
+ /// </summary>
public string DefaultUserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether playTo device profiles should be created.
+ /// </summary>
+ public bool AutoCreatePlayToProfiles { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to blast alive messages.
+ /// </summary>
+ public bool BlastAliveMessages { get; set; } = true;
+
+ /// <summary>
+ /// gets or sets a value indicating whether to send only matched host.
+ /// </summary>
+ public bool SendOnlyMatchedHost { get; set; } = true;
}
}
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
index f5a7eca72..916044a0c 100644
--- a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
+++ b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
@@ -9,11 +9,21 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
+ /// <summary>
+ /// Defines the <see cref="ConnectionManagerService" />.
+ /// </summary>
public class ConnectionManagerService : BaseService, IConnectionManager
{
private readonly IDlnaManager _dlna;
private readonly IServerConfigurationManager _config;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ConnectionManagerService"/> class.
+ /// </summary>
+ /// <param name="dlna">The <see cref="IDlnaManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
+ /// <param name="logger">The <see cref="ILogger{ConnectionManagerService}"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
+ /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
public ConnectionManagerService(
IDlnaManager dlna,
IServerConfigurationManager config,
@@ -28,7 +38,7 @@ namespace Emby.Dlna.ConnectionManager
/// <inheritdoc />
public string GetServiceXml()
{
- return new ConnectionManagerXmlBuilder().GetXml();
+ return ConnectionManagerXmlBuilder.GetXml();
}
/// <inheritdoc />
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
index c8db5a367..c484dac54 100644
--- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
+++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
@@ -6,45 +6,57 @@ using Emby.Dlna.Service;
namespace Emby.Dlna.ConnectionManager
{
- public class ConnectionManagerXmlBuilder
+ /// <summary>
+ /// Defines the <see cref="ConnectionManagerXmlBuilder" />.
+ /// </summary>
+ public static class ConnectionManagerXmlBuilder
{
- public string GetXml()
+ /// <summary>
+ /// Gets the ConnectionManager:1 service template.
+ /// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf.
+ /// </summary>
+ /// <returns>An XML description of this service.</returns>
+ public static string GetXml()
{
- return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), GetStateVariables());
+ return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
}
+ /// <summary>
+ /// Get the list of state variables for this invocation.
+ /// </summary>
+ /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables()
{
- var list = new List<StateVariable>();
-
- list.Add(new StateVariable
+ var list = new List<StateVariable>
{
- Name = "SourceProtocolInfo",
- DataType = "string",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "SourceProtocolInfo",
+ DataType = "string",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "SinkProtocolInfo",
- DataType = "string",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "SinkProtocolInfo",
+ DataType = "string",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "CurrentConnectionIDs",
- DataType = "string",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "CurrentConnectionIDs",
+ DataType = "string",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionStatus",
- DataType = "string",
- SendsEvents = false,
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ConnectionStatus",
+ DataType = "string",
+ SendsEvents = false,
- AllowedValues = new[]
+ AllowedValues = new[]
{
"OK",
"ContentFormatMismatch",
@@ -52,55 +64,56 @@ namespace Emby.Dlna.ConnectionManager
"UnreliableChannel",
"Unknown"
}
- });
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionManager",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ConnectionManager",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Direction",
- DataType = "string",
- SendsEvents = false,
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Direction",
+ DataType = "string",
+ SendsEvents = false,
- AllowedValues = new[]
+ AllowedValues = new[]
{
"Output",
"Input"
}
- });
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ProtocolInfo",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ProtocolInfo",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ConnectionID",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_AVTransportID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_AVTransportID",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_RcsID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_RcsID",
+ DataType = "ui4",
+ SendsEvents = false
+ }
+ };
return list;
}
diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs
index d4cc65394..2f8d197a7 100644
--- a/Emby.Dlna/ConnectionManager/ControlHandler.cs
+++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs
@@ -11,10 +11,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
+ /// <summary>
+ /// Defines the <see cref="ControlHandler" />.
+ /// </summary>
public class ControlHandler : BaseControlHandler
{
private readonly DeviceProfile _profile;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ControlHandler"/> class.
+ /// </summary>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param>
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
: base(config, logger)
{
@@ -33,6 +42,10 @@ namespace Emby.Dlna.ConnectionManager
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
}
+ /// <summary>
+ /// Builds the response to the GetProtocolInfo request.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private void HandleGetProtocolInfo(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
diff --git a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
index b853e7eab..542c7bfb4 100644
--- a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
+++ b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
@@ -5,9 +5,16 @@ using Emby.Dlna.Common;
namespace Emby.Dlna.ConnectionManager
{
- public class ServiceActionListBuilder
+ /// <summary>
+ /// Defines the <see cref="ServiceActionListBuilder" />.
+ /// </summary>
+ public static class ServiceActionListBuilder
{
- public IEnumerable<ServiceAction> GetActions()
+ /// <summary>
+ /// Returns an enumerable of the ConnectionManagar:1 DLNA actions.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
+ public static IEnumerable<ServiceAction> GetActions()
{
var list = new List<ServiceAction>
{
@@ -21,6 +28,10 @@ namespace Emby.Dlna.ConnectionManager
return list;
}
+ /// <summary>
+ /// Returns the action details for "PrepareForConnection".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction PrepareForConnection()
{
var action = new ServiceAction
@@ -80,6 +91,10 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
+ /// <summary>
+ /// Returns the action details for "GetCurrentConnectionInfo".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetCurrentConnectionInfo()
{
var action = new ServiceAction
@@ -146,7 +161,11 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
- private ServiceAction GetProtocolInfo()
+ /// <summary>
+ /// Returns the action details for "GetProtocolInfo".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction GetProtocolInfo()
{
var action = new ServiceAction
{
@@ -170,7 +189,11 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
- private ServiceAction GetCurrentConnectionIDs()
+ /// <summary>
+ /// Returns the action details for "GetCurrentConnectionIDs".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction GetCurrentConnectionIDs()
{
var action = new ServiceAction
{
@@ -187,7 +210,11 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
- private ServiceAction ConnectionComplete()
+ /// <summary>
+ /// Returns the action details for "ConnectionComplete".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction ConnectionComplete()
{
var action = new ServiceAction
{
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
index 5760f260c..2f3107450 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
@@ -19,6 +19,9 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory
{
+ /// <summary>
+ /// Defines the <see cref="ContentDirectoryService" />.
+ /// </summary>
public class ContentDirectoryService : BaseService, IContentDirectory
{
private readonly ILibraryManager _libraryManager;
@@ -33,6 +36,22 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ContentDirectoryService"/> class.
+ /// </summary>
+ /// <param name="dlna">The <see cref="IDlnaManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="userDataManager">The <see cref="IUserDataManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="imageProcessor">The <see cref="IImageProcessor"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="libraryManager">The <see cref="ILibraryManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="userManager">The <see cref="IUserManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="logger">The <see cref="ILogger{ContentDirectoryService}"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="httpClient">The <see cref="IHttpClientFactory"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="localization">The <see cref="ILocalizationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="userViewManager">The <see cref="IUserViewManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
+ /// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
public ContentDirectoryService(
IDlnaManager dlna,
IUserDataManager userDataManager,
@@ -62,7 +81,10 @@ namespace Emby.Dlna.ContentDirectory
_tvSeriesManager = tvSeriesManager;
}
- private int SystemUpdateId
+ /// <summary>
+ /// Gets the system id. (A unique id which changes on when our definition changes.)
+ /// </summary>
+ private static int SystemUpdateId
{
get
{
@@ -75,14 +97,18 @@ namespace Emby.Dlna.ContentDirectory
/// <inheritdoc />
public string GetServiceXml()
{
- return new ContentDirectoryXmlBuilder().GetXml();
+ return ContentDirectoryXmlBuilder.GetXml();
}
/// <inheritdoc />
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
- var profile = _dlna.GetProfile(request.Headers) ??
- _dlna.GetDefaultProfile();
+ if (request == null)
+ {
+ throw new ArgumentNullException(nameof(request));
+ }
+
+ var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
@@ -107,6 +133,11 @@ namespace Emby.Dlna.ContentDirectory
.ProcessControlRequestAsync(request);
}
+ /// <summary>
+ /// Get the user stored in the device profile.
+ /// </summary>
+ /// <param name="profile">The <see cref="DeviceProfile"/>.</param>
+ /// <returns>The <see cref="User"/>.</returns>
private User GetUser(DeviceProfile profile)
{
if (!string.IsNullOrEmpty(profile.UserId))
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
index 743dcc516..3edaabb70 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
@@ -6,143 +6,154 @@ using Emby.Dlna.Service;
namespace Emby.Dlna.ContentDirectory
{
- public class ContentDirectoryXmlBuilder
+ /// <summary>
+ /// Defines the <see cref="ContentDirectoryXmlBuilder" />.
+ /// </summary>
+ public static class ContentDirectoryXmlBuilder
{
- public string GetXml()
+ /// <summary>
+ /// Gets the ContentDirectory:1 service template.
+ /// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf.
+ /// </summary>
+ /// <returns>An XML description of this service.</returns>
+ public static string GetXml()
{
- return new ServiceXmlBuilder().GetXml(
- new ServiceActionListBuilder().GetActions(),
- GetStateVariables());
+ return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
}
+ /// <summary>
+ /// Get the list of state variables for this invocation.
+ /// </summary>
+ /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables()
{
- var list = new List<StateVariable>();
-
- list.Add(new StateVariable
+ var list = new List<StateVariable>
{
- Name = "A_ARG_TYPE_Filter",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Filter",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_SortCriteria",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_SortCriteria",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Index",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Index",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Count",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Count",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_UpdateID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_UpdateID",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "SearchCapabilities",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "SearchCapabilities",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "SortCapabilities",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "SortCapabilities",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "SystemUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "SystemUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_SearchCriteria",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_SearchCriteria",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Result",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Result",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_ObjectID",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_ObjectID",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_BrowseFlag",
- DataType = "string",
- SendsEvents = false,
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_BrowseFlag",
+ DataType = "string",
+ SendsEvents = false,
- AllowedValues = new[]
+ AllowedValues = new[]
{
"BrowseMetadata",
"BrowseDirectChildren"
}
- });
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_BrowseLetter",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_BrowseLetter",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_CategoryType",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_CategoryType",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_RID",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_RID",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_PosSec",
- DataType = "ui4",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_PosSec",
+ DataType = "ui4",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Featurelist",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Featurelist",
+ DataType = "string",
+ SendsEvents = false
+ }
+ };
return list;
}
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 5f25b8cdc..b93651746 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -1,6 +1,5 @@
-#pragma warning disable CS1591
-
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -8,6 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Emby.Dlna.Configuration;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
@@ -38,6 +38,9 @@ using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Dlna.ContentDirectory
{
+ /// <summary>
+ /// Defines the <see cref="ControlHandler" />.
+ /// </summary>
public class ControlHandler : BaseControlHandler
{
private const string NsDc = "http://purl.org/dc/elements/1.1/";
@@ -58,6 +61,24 @@ namespace Emby.Dlna.ContentDirectory
private readonly DeviceProfile _profile;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ControlHandler"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="libraryManager">The <see cref="ILibraryManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="serverAddress">The server address to use in this instance> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="accessToken">The <see cref="string"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="imageProcessor">The <see cref="IImageProcessor"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="userDataManager">The <see cref="IUserDataManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="user">The <see cref="User"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="systemUpdateId">The system id for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="localization">The <see cref="ILocalizationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="userViewManager">The <see cref="IUserViewManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> for use with the <see cref="ControlHandler"/> instance.</param>
+ /// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
public ControlHandler(
ILogger logger,
ILibraryManager libraryManager,
@@ -102,6 +123,16 @@ namespace Emby.Dlna.ContentDirectory
/// <inheritdoc />
protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
{
+ if (xmlWriter == null)
+ {
+ throw new ArgumentNullException(nameof(xmlWriter));
+ }
+
+ if (methodParams == null)
+ {
+ throw new ArgumentNullException(nameof(methodParams));
+ }
+
const string DeviceId = "test";
if (string.Equals(methodName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase))
@@ -167,6 +198,10 @@ namespace Emby.Dlna.ContentDirectory
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
}
+ /// <summary>
+ /// Adds a "XSetBookmark" element to the xml document.
+ /// </summary>
+ /// <param name="sparams">The <see cref="IDictionary"/>.</param>
private void HandleXSetBookmark(IDictionary<string, string> sparams)
{
var id = sparams["ObjectID"];
@@ -189,41 +224,69 @@ namespace Emby.Dlna.ContentDirectory
CancellationToken.None);
}
- private void HandleGetSearchCapabilities(XmlWriter xmlWriter)
+ /// <summary>
+ /// Adds the "SearchCaps" element to the xml document.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
+ private static void HandleGetSearchCapabilities(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString(
"SearchCaps",
"res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords");
}
- private void HandleGetSortCapabilities(XmlWriter xmlWriter)
+ /// <summary>
+ /// Adds the "SortCaps" element to the xml document.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
+ private static void HandleGetSortCapabilities(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString(
"SortCaps",
"res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating");
}
- private void HandleGetSortExtensionCapabilities(XmlWriter xmlWriter)
+ /// <summary>
+ /// Adds the "SortExtensionCaps" element to the xml document.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
+ private static void HandleGetSortExtensionCapabilities(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString(
"SortExtensionCaps",
"res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating");
}
+ /// <summary>
+ /// Adds the "Id" element to the xml document.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private void HandleGetSystemUpdateID(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString("Id", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
}
- private void HandleGetFeatureList(XmlWriter xmlWriter)
+ /// <summary>
+ /// Adds the "FeatureList" element to the xml document.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
+ private static void HandleGetFeatureList(XmlWriter xmlWriter)
{
xmlWriter.WriteElementString("FeatureList", WriteFeatureListXml());
}
- private void HandleXGetFeatureList(XmlWriter xmlWriter)
+ /// <summary>
+ /// Adds the "FeatureList" element to the xml document.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
+ private static void HandleXGetFeatureList(XmlWriter xmlWriter)
=> HandleGetFeatureList(xmlWriter);
- private string WriteFeatureListXml()
+ /// <summary>
+ /// Builds a static feature list.
+ /// </summary>
+ /// <returns>The xml feature list.</returns>
+ private static string WriteFeatureListXml()
{
// TODO: clean this up
var builder = new StringBuilder();
@@ -242,9 +305,16 @@ namespace Emby.Dlna.ContentDirectory
return builder.ToString();
}
- public string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
+ /// <summary>
+ /// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
+ /// </summary>
+ /// <param name="sparams">The <see cref="IDictionary"/>.</param>
+ /// <param name="key">The key.</param>
+ /// <param name="defaultValue">The defaultValue.</param>
+ /// <returns>The <see cref="string"/>.</returns>
+ public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
{
- if (sparams.TryGetValue(key, out string val))
+ if (sparams != null && sparams.TryGetValue(key, out string val))
{
return val;
}
@@ -252,6 +322,12 @@ namespace Emby.Dlna.ContentDirectory
return defaultValue;
}
+ /// <summary>
+ /// Builds the "Browse" xml response.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
+ /// <param name="sparams">The <see cref="IDictionary"/>.</param>
+ /// <param name="deviceId">The device Id to use.</param>
private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
{
var id = sparams["ObjectID"];
@@ -313,7 +389,6 @@ namespace Emby.Dlna.ContentDirectory
}
else
{
- var dlnaOptions = _config.GetDlnaConfiguration();
_didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
}
@@ -326,7 +401,6 @@ namespace Emby.Dlna.ContentDirectory
provided = childrenResult.Items.Count;
- var dlnaOptions = _config.GetDlnaConfiguration();
foreach (var i in childrenResult.Items)
{
var childItem = i.Item;
@@ -357,12 +431,24 @@ namespace Emby.Dlna.ContentDirectory
xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
}
+ /// <summary>
+ /// Builds the response to the "X_BrowseByLetter request.
+ /// </summary>
+ /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
+ /// <param name="sparams">The <see cref="IDictionary"/>.</param>
+ /// <param name="deviceId">The device id.</param>
private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
{
// TODO: Implement this method
HandleSearch(xmlWriter, sparams, deviceId);
}
+ /// <summary>
+ /// Builds a response to the "Search" request.
+ /// </summary>
+ /// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
+ /// <param name="sparams">The sparams<see cref="IDictionary"/>.</param>
+ /// <param name="deviceId">The deviceId<see cref="string"/>.</param>
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
{
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
@@ -442,7 +528,17 @@ namespace Emby.Dlna.ContentDirectory
xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
}
- private QueryResult<BaseItem> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
+ /// <summary>
+ /// Returns the child items meeting the criteria.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="search">The <see cref="SearchCriteria"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{BaseItem}"/>.</returns>
+ private static QueryResult<BaseItem> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
{
var folder = (Folder)item;
@@ -494,11 +590,25 @@ namespace Emby.Dlna.ContentDirectory
});
}
- private DtoOptions GetDtoOptions()
+ /// <summary>
+ /// Returns a new DtoOptions object.
+ /// </summary>
+ /// <returns>The <see cref="DtoOptions"/>.</returns>
+ private static DtoOptions GetDtoOptions()
{
return new DtoOptions(true);
}
+ /// <summary>
+ /// Returns the User items meeting the criteria.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="stubType">The <see cref="StubType"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)
{
if (item is MusicGenre)
@@ -568,6 +678,14 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(queryResult);
}
+ /// <summary>
+ /// Returns the Live Tv Channels meeting the criteria.
+ /// </summary>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetLiveTvChannels(User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -584,6 +702,16 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the music folders meeting the criteria.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="stubType">The <see cref="StubType"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -643,57 +771,58 @@ namespace Emby.Dlna.ContentDirectory
return GetMusicGenres(item, user, query);
}
- var list = new List<ServerItem>();
-
- list.Add(new ServerItem(item)
+ var list = new List<ServerItem>
{
- StubType = StubType.Latest
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Latest
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Playlists
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Playlists
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Albums
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Albums
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.AlbumArtists
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.AlbumArtists
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Artists
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Artists
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Songs
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Songs
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Genres
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Genres
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteArtists
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteArtists
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteAlbums
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteAlbums
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteSongs
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteSongs
+ }
+ };
return new QueryResult<ServerItem>
{
@@ -702,6 +831,16 @@ namespace Emby.Dlna.ContentDirectory
};
}
+ /// <summary>
+ /// Returns the movie folders meeting the criteria.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="stubType">The <see cref="StubType"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMovieFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -776,6 +915,13 @@ namespace Emby.Dlna.ContentDirectory
};
}
+ /// <summary>
+ /// Returns the folders meeting the criteria.
+ /// </summary>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetFolders(User user, int? startIndex, int? limit)
{
var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true)
@@ -796,6 +942,16 @@ namespace Emby.Dlna.ContentDirectory
limit);
}
+ /// <summary>
+ /// Returns the TV folders meeting the criteria.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="stubType">The <see cref="StubType"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -840,42 +996,43 @@ namespace Emby.Dlna.ContentDirectory
return GetGenres(item, user, query);
}
- var list = new List<ServerItem>();
-
- list.Add(new ServerItem(item)
+ var list = new List<ServerItem>
{
- StubType = StubType.ContinueWatching
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.ContinueWatching
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.NextUp
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.NextUp
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Latest
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Latest
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Series
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Series
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteSeries
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteSeries
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.FavoriteEpisodes
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.FavoriteEpisodes
+ },
- list.Add(new ServerItem(item)
- {
- StubType = StubType.Genres
- });
+ new ServerItem(item)
+ {
+ StubType = StubType.Genres
+ }
+ };
return new QueryResult<ServerItem>
{
@@ -884,6 +1041,13 @@ namespace Emby.Dlna.ContentDirectory
};
}
+ /// <summary>
+ /// Returns the Movies that are part watched that meet the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMovieContinueWatching(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -904,6 +1068,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the series meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetSeries(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -917,6 +1088,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the Movie folders meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMovieMovies(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -930,6 +1108,12 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the Movie collections meeting the criteria.
+ /// </summary>
+ /// <param name="user">The see cref="User"/>.</param>
+ /// <param name="query">The see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -943,6 +1127,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the Music albums meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicAlbums(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -956,6 +1147,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the Music songs meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicSongs(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -969,6 +1167,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the songs tagged as favourite that meet the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetFavoriteSongs(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -982,6 +1187,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the series tagged as favourite that meet the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetFavoriteSeries(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -995,6 +1207,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the episodes tagged as favourite that meet the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetFavoriteEpisodes(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -1008,6 +1227,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the movies tagged as favourite that meet the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMovieFavorites(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -1021,6 +1247,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// /// Returns the albums tagged as favourite that meet the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetFavoriteAlbums(BaseItem parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
@@ -1034,6 +1267,14 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the genres meeting the criteria.
+ /// The GetGenres.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetGenres(BaseItem parent, User user, InternalItemsQuery query)
{
var genresResult = _libraryManager.GetGenres(new InternalItemsQuery(user)
@@ -1052,6 +1293,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the music genres meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicGenres(BaseItem parent, User user, InternalItemsQuery query)
{
var genresResult = _libraryManager.GetMusicGenres(new InternalItemsQuery(user)
@@ -1070,6 +1318,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the music albums by artist that meet the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicAlbumArtists(BaseItem parent, User user, InternalItemsQuery query)
{
var artists = _libraryManager.GetAlbumArtists(new InternalItemsQuery(user)
@@ -1088,6 +1343,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the music artists meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicArtists(BaseItem parent, User user, InternalItemsQuery query)
{
var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
@@ -1106,6 +1368,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the artists tagged as favourite that meet the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetFavoriteArtists(BaseItem parent, User user, InternalItemsQuery query)
{
var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
@@ -1125,6 +1394,12 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the music playlists meeting the criteria.
+ /// </summary>
+ /// <param name="user">The user<see cref="User"/>.</param>
+ /// <param name="query">The query<see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
{
query.Parent = null;
@@ -1137,6 +1412,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the latest music meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicLatest(BaseItem parent, User user, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1155,6 +1437,12 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(items);
}
+ /// <summary>
+ /// Returns the next up item meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetNextUp(BaseItem parent, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1172,6 +1460,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the latest tv meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetTvLatest(BaseItem parent, User user, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1190,6 +1485,13 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(items);
}
+ /// <summary>
+ /// Returns the latest movies meeting the criteria.
+ /// </summary>
+ /// <param name="parent">The <see cref="BaseItem"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMovieLatest(BaseItem parent, User user, InternalItemsQuery query)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1208,6 +1510,16 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(items);
}
+ /// <summary>
+ /// Returns music artist items that meet the criteria.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="parentId">The <see cref="Guid"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicArtistItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -1228,6 +1540,16 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the genre items meeting the criteria.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="parentId">The <see cref="Guid"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -1252,6 +1574,16 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
+ /// <summary>
+ /// Returns the music genre items meeting the criteria.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="parentId">The <see cref="Guid"/>.</param>
+ /// <param name="user">The <see cref="User"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetMusicGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
@@ -1272,7 +1604,12 @@ namespace Emby.Dlna.ContentDirectory
return ToResult(result);
}
- private QueryResult<ServerItem> ToResult(BaseItem[] result)
+ /// <summary>
+ /// Converts a <see cref="BaseItem"/> array into a <see cref="QueryResult{ServerItem}"/>.
+ /// </summary>
+ /// <param name="result">An array of <see cref="BaseItem"/>.</param>
+ /// <returns>A <see cref="QueryResult{ServerItem}"/>.</returns>
+ private static QueryResult<ServerItem> ToResult(BaseItem[] result)
{
var serverItems = result
.Select(i => new ServerItem(i))
@@ -1285,7 +1622,12 @@ namespace Emby.Dlna.ContentDirectory
};
}
- private QueryResult<ServerItem> ToResult(QueryResult<BaseItem> result)
+ /// <summary>
+ /// Converts a <see cref="QueryResult{BaseItem}"/> to a <see cref="QueryResult{ServerItem}"/>.
+ /// </summary>
+ /// <param name="result">A <see cref="QueryResult{BaseItem}"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
+ private static QueryResult<ServerItem> ToResult(QueryResult<BaseItem> result)
{
var serverItems = result
.Items
@@ -1299,7 +1641,13 @@ namespace Emby.Dlna.ContentDirectory
};
}
- private void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted)
+ /// <summary>
+ /// Sets the sorting method on a query.
+ /// </summary>
+ /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="isPreSorted">True if pre-sorted.</param>
+ private static void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted)
{
if (isPreSorted)
{
@@ -1311,13 +1659,25 @@ namespace Emby.Dlna.ContentDirectory
}
}
- private QueryResult<ServerItem> ApplyPaging(QueryResult<ServerItem> result, int? startIndex, int? limit)
+ /// <summary>
+ /// Apply paging to a query.
+ /// </summary>
+ /// <param name="result">The <see cref="QueryResult{ServerItem}"/>.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The maximum number to return.</param>
+ /// <returns>A <see cref="QueryResult{ServerItem}"/>.</returns>
+ private static QueryResult<ServerItem> ApplyPaging(QueryResult<ServerItem> result, int? startIndex, int? limit)
{
result.Items = result.Items.Skip(startIndex ?? 0).Take(limit ?? int.MaxValue).ToArray();
return result;
}
+ /// <summary>
+ /// Retrieves the ServerItem id.
+ /// </summary>
+ /// <param name="id">The id<see cref="string"/>.</param>
+ /// <returns>The <see cref="ServerItem"/>.</returns>
private ServerItem GetItemFromObjectId(string id)
{
return DidlBuilder.IsIdRoot(id)
@@ -1326,6 +1686,11 @@ namespace Emby.Dlna.ContentDirectory
: ParseItemId(id);
}
+ /// <summary>
+ /// Parses the item id into a <see cref="ServerItem"/>.
+ /// </summary>
+ /// <param name="id">The <see cref="string"/>.</param>
+ /// <returns>The corresponding <see cref="ServerItem"/>.</returns>
private ServerItem ParseItemId(string id)
{
StubType? stubType = null;
diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs
index e40605414..34244000c 100644
--- a/Emby.Dlna/ContentDirectory/ServerItem.cs
+++ b/Emby.Dlna/ContentDirectory/ServerItem.cs
@@ -4,8 +4,15 @@ using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
{
+ /// <summary>
+ /// Defines the <see cref="ServerItem" />.
+ /// </summary>
internal class ServerItem
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ServerItem"/> class.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
public ServerItem(BaseItem item)
{
Item = item;
@@ -16,8 +23,14 @@ namespace Emby.Dlna.ContentDirectory
}
}
+ /// <summary>
+ /// Gets or sets the underlying base item.
+ /// </summary>
public BaseItem Item { get; set; }
+ /// <summary>
+ /// Gets or sets the DLNA item type.
+ /// </summary>
public StubType? StubType { get; set; }
}
}
diff --git a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
index 921b14e39..7e3db4651 100644
--- a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
+++ b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
@@ -1,13 +1,18 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using Emby.Dlna.Common;
namespace Emby.Dlna.ContentDirectory
{
- public class ServiceActionListBuilder
+ /// <summary>
+ /// Defines the <see cref="ServiceActionListBuilder" />.
+ /// </summary>
+ public static class ServiceActionListBuilder
{
- public IEnumerable<ServiceAction> GetActions()
+ /// <summary>
+ /// Returns a list of services that this instance provides.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
+ public static IEnumerable<ServiceAction> GetActions()
{
return new[]
{
@@ -22,6 +27,10 @@ namespace Emby.Dlna.ContentDirectory
};
}
+ /// <summary>
+ /// Returns the action details for "GetSystemUpdateID".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetSystemUpdateIDAction()
{
var action = new ServiceAction
@@ -39,6 +48,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
+ /// <summary>
+ /// Returns the action details for "GetSearchCapabilities".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSearchCapabilitiesAction()
{
var action = new ServiceAction
@@ -56,6 +69,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
+ /// <summary>
+ /// Returns the action details for "GetSortCapabilities".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSortCapabilitiesAction()
{
var action = new ServiceAction
@@ -73,6 +90,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
+ /// <summary>
+ /// Returns the action details for "X_GetFeatureList".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetX_GetFeatureListAction()
{
var action = new ServiceAction
@@ -90,6 +111,10 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
+ /// <summary>
+ /// Returns the action details for "Search".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetSearchAction()
{
var action = new ServiceAction
@@ -170,7 +195,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
- private ServiceAction GetBrowseAction()
+ /// <summary>
+ /// Returns the action details for "Browse".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction GetBrowseAction()
{
var action = new ServiceAction
{
@@ -250,7 +279,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
- private ServiceAction GetBrowseByLetterAction()
+ /// <summary>
+ /// Returns the action details for "X_BrowseByLetter".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction GetBrowseByLetterAction()
{
var action = new ServiceAction
{
@@ -337,7 +370,11 @@ namespace Emby.Dlna.ContentDirectory
return action;
}
- private ServiceAction GetXSetBookmarkAction()
+ /// <summary>
+ /// Returns the action details for "X_SetBookmark".
+ /// </summary>
+ /// <returns>The <see cref="ServiceAction"/>.</returns>
+ private static ServiceAction GetXSetBookmarkAction()
{
var action = new ServiceAction
{
diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs
index eee405d3e..982ae5d68 100644
--- a/Emby.Dlna/ContentDirectory/StubType.cs
+++ b/Emby.Dlna/ContentDirectory/StubType.cs
@@ -3,6 +3,9 @@
namespace Emby.Dlna.ContentDirectory
{
+ /// <summary>
+ /// Defines the DLNA item types.
+ /// </summary>
public enum StubType
{
Folder = 0,
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 069400833..fedd20b68 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -484,10 +484,10 @@ namespace Emby.Dlna
/// <summary>
/// Recreates the object using serialization, to ensure it's not a subclass.
- /// If it's a subclass it may not serlialize properly to xml (different root element tag name).
+ /// If it's a subclass it may not serialize properly to xml (different root element tag name).
/// </summary>
/// <param name="profile">The device profile.</param>
- /// <returns>The reserialized device profile.</returns>
+ /// <returns>The re-serialized device profile.</returns>
private DeviceProfile ReserializeProfile(DeviceProfile profile)
{
if (profile.GetType() == typeof(DeviceProfile))
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 6ed49944c..bd30cc1e1 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -17,7 +17,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index c97acdb02..f8ff03076 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -480,7 +480,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
+ // If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive
if (transportState.Value == TransportState.Stopped)
{
RestartTimerInactive();
@@ -775,7 +775,7 @@ namespace Emby.Dlna.PlayTo
if (track == null)
{
- // If track is null, some vendors do this, use GetMediaInfo instead
+ // If track is null, some vendors do this, use GetMediaInfo instead.
return (true, null);
}
@@ -812,7 +812,7 @@ namespace Emby.Dlna.PlayTo
private XElement ParseResponse(string xml)
{
- // Handle different variations sent back by devices
+ // Handle different variations sent back by devices.
try
{
return XElement.Parse(xml);
@@ -821,7 +821,7 @@ namespace Emby.Dlna.PlayTo
{
}
- // first try to add a root node with a dlna namesapce
+ // first try to add a root node with a dlna namespace.
try
{
return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>")
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index c07c8aefa..3907b2a39 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -945,7 +945,10 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
- request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
+
+ // Be careful, IsDirectStream==true by default (Static != false or not in query).
+ // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
+ request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index c8c36fc97..f4d793790 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -45,7 +45,7 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
@@ -94,7 +94,7 @@ namespace Emby.Dlna.PlayTo
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index 092f8580a..7d479a5c6 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs
index b63be3a64..bbfdccc90 100644
--- a/Emby.Naming/Audio/AlbumParser.cs
+++ b/Emby.Naming/Audio/AlbumParser.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.Globalization;
using System.IO;
@@ -9,15 +6,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.Audio
{
+ /// <summary>
+ /// Helper class to determine if Album is multipart.
+ /// </summary>
public class AlbumParser
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AlbumParser"/> class.
+ /// </summary>
+ /// <param name="options">Naming options containing AlbumStackingPrefixes.</param>
public AlbumParser(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Function that determines if album is multipart.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <returns>True if album is multipart.</returns>
public bool IsMultiPart(string path)
{
var filename = Path.GetFileName(path);
diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs
index 6b2f4be93..8b47dd12e 100644
--- a/Emby.Naming/Audio/AudioFileParser.cs
+++ b/Emby.Naming/Audio/AudioFileParser.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -8,8 +5,17 @@ using Emby.Naming.Common;
namespace Emby.Naming.Audio
{
+ /// <summary>
+ /// Static helper class to determine if file at path is audio file.
+ /// </summary>
public static class AudioFileParser
{
+ /// <summary>
+ /// Static helper method to determine if file at path is audio file.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <param name="options"><see cref="NamingOptions"/> containing AudioFileExtensions.</param>
+ /// <returns>True if file at path is audio file.</returns>
public static bool IsAudioFile(string path, NamingOptions options)
{
var extension = Path.GetExtension(path);
diff --git a/Emby.Naming/AudioBook/AudioBookFileInfo.cs b/Emby.Naming/AudioBook/AudioBookFileInfo.cs
index c4863b50a..862e39667 100644
--- a/Emby.Naming/AudioBook/AudioBookFileInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookFileInfo.cs
@@ -8,6 +8,21 @@ namespace Emby.Naming.AudioBook
public class AudioBookFileInfo : IComparable<AudioBookFileInfo>
{
/// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookFileInfo"/> class.
+ /// </summary>
+ /// <param name="path">Path to audiobook file.</param>
+ /// <param name="container">File type.</param>
+ /// <param name="partNumber">Number of part this file represents.</param>
+ /// <param name="chapterNumber">Number of chapter this file represents.</param>
+ public AudioBookFileInfo(string path, string container, int? partNumber = default, int? chapterNumber = default)
+ {
+ Path = path;
+ Container = container;
+ PartNumber = partNumber;
+ ChapterNumber = chapterNumber;
+ }
+
+ /// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
@@ -31,14 +46,8 @@ namespace Emby.Naming.AudioBook
/// <value>The chapter number.</value>
public int? ChapterNumber { get; set; }
- /// <summary>
- /// Gets or sets a value indicating whether this instance is a directory.
- /// </summary>
- /// <value>The type.</value>
- public bool IsDirectory { get; set; }
-
/// <inheritdoc />
- public int CompareTo(AudioBookFileInfo other)
+ public int CompareTo(AudioBookFileInfo? other)
{
if (ReferenceEquals(this, other))
{
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
index 14edd6492..7b4429ab1 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
@@ -8,15 +5,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
+ /// <summary>
+ /// Parser class to extract part and/or chapter number from audiobook filename.
+ /// </summary>
public class AudioBookFilePathParser
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookFilePathParser"/> class.
+ /// </summary>
+ /// <param name="options">Naming options containing AudioBookPartsExpressions.</param>
public AudioBookFilePathParser(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Based on regex determines if filename includes part/chapter number.
+ /// </summary>
+ /// <param name="path">Path to audiobook file.</param>
+ /// <returns>Returns <see cref="AudioBookFilePathParser"/> object.</returns>
public AudioBookFilePathParserResult Parse(string path)
{
AudioBookFilePathParserResult result = default;
@@ -52,8 +61,6 @@ namespace Emby.Naming.AudioBook
}
}
- 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 7bfc4479d..48ab8b57d 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
@@ -1,14 +1,18 @@
-#nullable enable
-#pragma warning disable CS1591
-
namespace Emby.Naming.AudioBook
{
+ /// <summary>
+ /// Data object for passing result of audiobook part/chapter extraction.
+ /// </summary>
public struct AudioBookFilePathParserResult
{
+ /// <summary>
+ /// Gets or sets optional number of path extracted from audiobook filename.
+ /// </summary>
public int? PartNumber { get; set; }
+ /// <summary>
+ /// Gets or sets optional number of chapter extracted from audiobook filename.
+ /// </summary>
public int? ChapterNumber { get; set; }
-
- public bool Success { get; set; }
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs
index b0b5bd881..adf403ab6 100644
--- a/Emby.Naming/AudioBook/AudioBookInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookInfo.cs
@@ -10,11 +10,18 @@ namespace Emby.Naming.AudioBook
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookInfo" /> class.
/// </summary>
- public AudioBookInfo()
+ /// <param name="name">Name of audiobook.</param>
+ /// <param name="year">Year of audiobook release.</param>
+ /// <param name="files">List of files composing the actual audiobook.</param>
+ /// <param name="extras">List of extra files.</param>
+ /// <param name="alternateVersions">Alternative version of files.</param>
+ public AudioBookInfo(string name, int? year, List<AudioBookFileInfo>? files, List<AudioBookFileInfo>? extras, List<AudioBookFileInfo>? alternateVersions)
{
- Files = new List<AudioBookFileInfo>();
- Extras = new List<AudioBookFileInfo>();
- AlternateVersions = new List<AudioBookFileInfo>();
+ Name = name;
+ Year = year;
+ Files = files ?? new List<AudioBookFileInfo>();
+ Extras = extras ?? new List<AudioBookFileInfo>();
+ AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>();
}
/// <summary>
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index f4ba11a0d..e9ea9b7a5 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -1,6 +1,6 @@
-#pragma warning disable CS1591
-
+using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@@ -8,40 +8,145 @@ using MediaBrowser.Model.IO;
namespace Emby.Naming.AudioBook
{
+ /// <summary>
+ /// Class used to resolve Name, Year, alternative files and extras from stack of files.
+ /// </summary>
public class AudioBookListResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
+ /// </summary>
+ /// <param name="options">Naming options passed along to <see cref="AudioBookResolver"/> and <see cref="AudioBookNameParser"/>.</param>
public AudioBookListResolver(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Resolves Name, Year and differentiate alternative files and extras from regular audiobook files.
+ /// </summary>
+ /// <param name="files">List of files related to audiobook.</param>
+ /// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
{
var audioBookResolver = new AudioBookResolver(_options);
+ // File with empty fullname will be sorted out here.
var audiobookFileInfos = files
- .Select(i => audioBookResolver.Resolve(i.FullName, i.IsDirectory))
- .Where(i => i != null)
+ .Select(i => audioBookResolver.Resolve(i.FullName))
+ .OfType<AudioBookFileInfo>()
.ToList();
- // Filter out all extras, otherwise they could cause stacks to not be resolved
- // See the unit test TestStackedWithTrailer
- var metadata = audiobookFileInfos
- .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
-
var stackResult = new StackResolver(_options)
- .ResolveAudioBooks(metadata);
+ .ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
- var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList();
+ var stackFiles = stack.Files
+ .Select(i => audioBookResolver.Resolve(i))
+ .OfType<AudioBookFileInfo>()
+ .ToList();
+
stackFiles.Sort();
- var info = new AudioBookInfo { Files = stackFiles, Name = stack.Name };
+
+ var nameParserResult = new AudioBookNameParser(_options).Parse(stack.Name);
+
+ FindExtraAndAlternativeFiles(ref stackFiles, out var extras, out var alternativeVersions, nameParserResult);
+
+ var info = new AudioBookInfo(
+ nameParserResult.Name,
+ nameParserResult.Year,
+ stackFiles,
+ extras,
+ alternativeVersions);
yield return info;
}
}
+
+ private void FindExtraAndAlternativeFiles(ref List<AudioBookFileInfo> stackFiles, out List<AudioBookFileInfo> extras, out List<AudioBookFileInfo> alternativeVersions, AudioBookNameParserResult nameParserResult)
+ {
+ extras = new List<AudioBookFileInfo>();
+ alternativeVersions = new List<AudioBookFileInfo>();
+
+ var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
+ var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
+ var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
+
+ foreach (var group in groupedBy)
+ {
+ if (group.Key.ChapterNumber == null && group.Key.PartNumber == null)
+ {
+ if (group.Count() > 1 || haveChaptersOrPages)
+ {
+ var ex = new List<AudioBookFileInfo>();
+ var alt = new List<AudioBookFileInfo>();
+
+ foreach (var audioFile in group)
+ {
+ var name = Path.GetFileNameWithoutExtension(audioFile.Path);
+ if (name.Equals("audiobook") ||
+ name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
+ name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
+ {
+ alt.Add(audioFile);
+ }
+ else
+ {
+ ex.Add(audioFile);
+ }
+ }
+
+ if (ex.Count > 0)
+ {
+ var extra = ex
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .ToList();
+
+ stackFiles = stackFiles.Except(extra).ToList();
+ extras.AddRange(extra);
+ }
+
+ if (alt.Count > 0)
+ {
+ var alternatives = alt
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .ToList();
+
+ var main = FindMainAudioBookFile(alternatives, nameParserResult.Name);
+ alternatives.Remove(main);
+ stackFiles = stackFiles.Except(alternatives).ToList();
+ alternativeVersions.AddRange(alternatives);
+ }
+ }
+ }
+ else if (group.Count() > 1)
+ {
+ var alternatives = group
+ .OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .Skip(1)
+ .ToList();
+
+ stackFiles = stackFiles.Except(alternatives).ToList();
+ alternativeVersions.AddRange(alternatives);
+ }
+ }
+ }
+
+ private AudioBookFileInfo FindMainAudioBookFile(List<AudioBookFileInfo> files, string name)
+ {
+ var main = files.Find(x => Path.GetFileNameWithoutExtension(x.Path).Equals(name, StringComparison.OrdinalIgnoreCase));
+ main ??= files.FirstOrDefault(x => Path.GetFileNameWithoutExtension(x.Path).Equals("audiobook", StringComparison.OrdinalIgnoreCase));
+ main ??= files.OrderBy(x => x.Container)
+ .ThenBy(x => x.Path)
+ .First();
+
+ return main;
+ }
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs
new file mode 100644
index 000000000..120482bc2
--- /dev/null
+++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs
@@ -0,0 +1,67 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.AudioBook
+{
+ /// <summary>
+ /// Helper class to retrieve name and year from audiobook previously retrieved name.
+ /// </summary>
+ public class AudioBookNameParser
+ {
+ private readonly NamingOptions _options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookNameParser"/> class.
+ /// </summary>
+ /// <param name="options">Naming options containing AudioBookNamesExpressions.</param>
+ public AudioBookNameParser(NamingOptions options)
+ {
+ _options = options;
+ }
+
+ /// <summary>
+ /// Parse name and year from previously determined name of audiobook.
+ /// </summary>
+ /// <param name="name">Name of the audiobook.</param>
+ /// <returns>Returns <see cref="AudioBookNameParserResult"/> object.</returns>
+ public AudioBookNameParserResult Parse(string name)
+ {
+ AudioBookNameParserResult result = default;
+ foreach (var expression in _options.AudioBookNamesExpressions)
+ {
+ var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
+ if (match.Success)
+ {
+ if (result.Name == null)
+ {
+ var value = match.Groups["name"];
+ if (value.Success)
+ {
+ result.Name = value.Value;
+ }
+ }
+
+ if (!result.Year.HasValue)
+ {
+ var value = match.Groups["year"];
+ if (value.Success)
+ {
+ if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ {
+ result.Year = intValue;
+ }
+ }
+ }
+ }
+ }
+
+ if (string.IsNullOrEmpty(result.Name))
+ {
+ result.Name = name;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Emby.Naming/AudioBook/AudioBookNameParserResult.cs b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
new file mode 100644
index 000000000..3f2d7b2b0
--- /dev/null
+++ b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
@@ -0,0 +1,18 @@
+namespace Emby.Naming.AudioBook
+{
+ /// <summary>
+ /// Data object used to pass result of name and year parsing.
+ /// </summary>
+ public struct AudioBookNameParserResult
+ {
+ /// <summary>
+ /// Gets or sets name of audiobook.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets optional year of release.
+ /// </summary>
+ public int? Year { get; set; }
+ }
+}
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index 5807d4688..f6ad3601d 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -8,25 +5,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.AudioBook
{
+ /// <summary>
+ /// Resolve specifics (path, container, partNumber, chapterNumber) about audiobook file.
+ /// </summary>
public class AudioBookResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioBookResolver"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> containing AudioFileExtensions and also used to pass to AudioBookFilePathParser.</param>
public AudioBookResolver(NamingOptions options)
{
_options = options;
}
- public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
+ /// <summary>
+ /// Resolve specifics (path, container, partNumber, chapterNumber) about audiobook file.
+ /// </summary>
+ /// <param name="path">Path to audiobook file.</param>
+ /// <returns>Returns <see cref="AudioBookResolver"/> object.</returns>
+ public AudioBookFileInfo? Resolve(string path)
{
- if (path.Length == 0)
- {
- throw new ArgumentException("String can't be empty.", nameof(path));
- }
-
- // TODO
- if (isDirectory)
+ if (path.Length == 0 || Path.GetFileNameWithoutExtension(path).Length == 0)
{
+ // Return null to indicate this path will not be used, instead of stopping whole process with exception
return null;
}
@@ -42,14 +46,11 @@ namespace Emby.Naming.AudioBook
var parsingResult = new AudioBookFilePathParser(_options).Parse(path);
- return new AudioBookFileInfo
- {
- Path = path,
- Container = container,
- ChapterNumber = parsingResult.ChapterNumber,
- PartNumber = parsingResult.PartNumber,
- IsDirectory = isDirectory
- };
+ return new AudioBookFileInfo(
+ path,
+ container,
+ chapterNumber: parsingResult.ChapterNumber,
+ partNumber: parsingResult.PartNumber);
}
}
}
diff --git a/Emby.Naming/Common/EpisodeExpression.cs b/Emby.Naming/Common/EpisodeExpression.cs
index ed6ba8881..19d3c7aab 100644
--- a/Emby.Naming/Common/EpisodeExpression.cs
+++ b/Emby.Naming/Common/EpisodeExpression.cs
@@ -1,28 +1,32 @@
-#pragma warning disable CS1591
-
using System;
using System.Text.RegularExpressions;
namespace Emby.Naming.Common
{
+ /// <summary>
+ /// Regular expressions for parsing TV Episodes.
+ /// </summary>
public class EpisodeExpression
{
private string _expression;
- private Regex _regex;
+ private Regex? _regex;
- public EpisodeExpression(string expression, bool byDate)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpisodeExpression"/> class.
+ /// </summary>
+ /// <param name="expression">Regular expressions.</param>
+ /// <param name="byDate">True if date is expected.</param>
+ public EpisodeExpression(string expression, bool byDate = false)
{
- Expression = expression;
+ _expression = expression;
IsByDate = byDate;
DateTimeFormats = Array.Empty<string>();
SupportsAbsoluteEpisodeNumbers = true;
}
- public EpisodeExpression(string expression)
- : this(expression, false)
- {
- }
-
+ /// <summary>
+ /// Gets or sets raw expressions string.
+ /// </summary>
public string Expression
{
get => _expression;
@@ -33,16 +37,34 @@ namespace Emby.Naming.Common
}
}
+ /// <summary>
+ /// Gets or sets a value indicating whether gets or sets property indicating if date can be find in expression.
+ /// </summary>
public bool IsByDate { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether gets or sets property indicating if expression is optimistic.
+ /// </summary>
public bool IsOptimistic { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether gets or sets property indicating if expression is named.
+ /// </summary>
public bool IsNamed { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether gets or sets property indicating if expression supports episodes with absolute numbers.
+ /// </summary>
public bool SupportsAbsoluteEpisodeNumbers { get; set; }
+ /// <summary>
+ /// Gets or sets optional list of date formats used for date parsing.
+ /// </summary>
public string[] DateTimeFormats { get; set; }
+ /// <summary>
+ /// Gets a <see cref="Regex"/> expressions objects (creates it if null).
+ /// </summary>
public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
}
diff --git a/Emby.Naming/Common/MediaType.cs b/Emby.Naming/Common/MediaType.cs
index 148833765..dc9784c6d 100644
--- a/Emby.Naming/Common/MediaType.cs
+++ b/Emby.Naming/Common/MediaType.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Common
{
+ /// <summary>
+ /// Type of audiovisual media.
+ /// </summary>
public enum MediaType
{
/// <summary>
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index fd4244f64..035d1b228 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -1,15 +1,21 @@
-#pragma warning disable CS1591
-
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
+// ReSharper disable StringLiteralTypo
+
namespace Emby.Naming.Common
{
+ /// <summary>
+ /// Big ugly class containing lot of different naming options that should be split and injected instead of passes everywhere.
+ /// </summary>
public class NamingOptions
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NamingOptions"/> class.
+ /// </summary>
public NamingOptions()
{
VideoFileExtensions = new[]
@@ -75,63 +81,52 @@ namespace Emby.Naming.Common
StubTypes = new[]
{
- new StubTypeRule
- {
- StubType = "dvd",
- Token = "dvd"
- },
- new StubTypeRule
- {
- StubType = "hddvd",
- Token = "hddvd"
- },
- new StubTypeRule
- {
- StubType = "bluray",
- Token = "bluray"
- },
- new StubTypeRule
- {
- StubType = "bluray",
- Token = "brrip"
- },
- new StubTypeRule
- {
- StubType = "bluray",
- Token = "bd25"
- },
- new StubTypeRule
- {
- StubType = "bluray",
- Token = "bd50"
- },
- new StubTypeRule
- {
- StubType = "vhs",
- Token = "vhs"
- },
- new StubTypeRule
- {
- StubType = "tv",
- Token = "HDTV"
- },
- new StubTypeRule
- {
- StubType = "tv",
- Token = "PDTV"
- },
- new StubTypeRule
- {
- StubType = "tv",
- Token = "DSR"
- }
+ new StubTypeRule(
+ stubType: "dvd",
+ token: "dvd"),
+
+ new StubTypeRule(
+ stubType: "hddvd",
+ token: "hddvd"),
+
+ new StubTypeRule(
+ stubType: "bluray",
+ token: "bluray"),
+
+ new StubTypeRule(
+ stubType: "bluray",
+ token: "brrip"),
+
+ new StubTypeRule(
+ stubType: "bluray",
+ token: "bd25"),
+
+ new StubTypeRule(
+ stubType: "bluray",
+ token: "bd50"),
+
+ new StubTypeRule(
+ stubType: "vhs",
+ token: "vhs"),
+
+ new StubTypeRule(
+ stubType: "tv",
+ token: "HDTV"),
+
+ new StubTypeRule(
+ stubType: "tv",
+ token: "PDTV"),
+
+ new StubTypeRule(
+ stubType: "tv",
+ token: "DSR")
};
VideoFileStackingExpressions = new[]
{
- "(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$",
- "(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(.*?)(\\.[^.]+)$",
- "(.*?)([ ._-]*[a-d])(.*?)(\\.[^.]+)$"
+ "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
+ "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
+ "(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
};
CleanDateTimes = new[]
@@ -142,7 +137,7 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
- @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+ @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])"
};
@@ -255,7 +250,7 @@ namespace Emby.Naming.Common
},
// <!-- foo.ep01, foo.EP_01 -->
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
- new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true)
+ new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
{
@@ -264,7 +259,7 @@ namespace Emby.Naming.Common
"yyyy_MM_dd"
}
},
- new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true)
+ new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[0-9]{4})", true)
{
DateTimeFormats = new[]
{
@@ -286,7 +281,12 @@ namespace Emby.Naming.Common
{
SupportsAbsoluteEpisodeNumbers = true
},
- new EpisodeExpression(@"[\\\\/\\._ -](?<seriesname>(?![0-9]+[0-9][0-9])([^\\\/])*)[\\\\/\\._ -](?<seasonnumber>[0-9]+)(?<epnumber>[0-9][0-9](?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([\\._ -][^\\\\/]*)$")
+
+ // Case Closed (1996-2007)/Case Closed - 317.mkv
+ // /server/anything_102.mp4
+ // /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
+ // /server/anything_1996.11.14.mp4
+ new EpisodeExpression(@"[\\/._ -](?<seriesname>(?![0-9]+[0-9][0-9])([^\\\/_])*)[\\\/._ -](?<seasonnumber>[0-9]+)(?<epnumber>[0-9][0-9](?:(?:[a-i]|\.[1-9])(?![0-9]))?)([._ -][^\\\/]*)$")
{
IsOptimistic = true,
IsNamed = true,
@@ -381,247 +381,193 @@ namespace Emby.Naming.Common
VideoExtraRules = new[]
{
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Filename,
- Token = "trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Suffix,
- Token = "-trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Suffix,
- Token = ".trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Suffix,
- Token = "_trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Trailer,
- RuleType = ExtraRuleType.Suffix,
- Token = " trailer",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Filename,
- Token = "sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Suffix,
- Token = "-sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Suffix,
- Token = ".sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Suffix,
- Token = "_sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.Suffix,
- Token = " sample",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.ThemeSong,
- RuleType = ExtraRuleType.Filename,
- Token = "theme",
- MediaType = MediaType.Audio
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Scene,
- RuleType = ExtraRuleType.Suffix,
- Token = "-scene",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.Suffix,
- Token = "-clip",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Interview,
- RuleType = ExtraRuleType.Suffix,
- Token = "-interview",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.BehindTheScenes,
- RuleType = ExtraRuleType.Suffix,
- Token = "-behindthescenes",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.DeletedScene,
- RuleType = ExtraRuleType.Suffix,
- Token = "-deleted",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.Suffix,
- Token = "-featurette",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.Suffix,
- Token = "-short",
- MediaType = MediaType.Video
- },
- new ExtraRule
- {
- ExtraType = ExtraType.BehindTheScenes,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "behind the scenes",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.DeletedScene,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "deleted scenes",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Interview,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "interviews",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Scene,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "scenes",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Sample,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "samples",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "shorts",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Clip,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "featurettes",
- MediaType = MediaType.Video,
- },
- new ExtraRule
- {
- ExtraType = ExtraType.Unknown,
- RuleType = ExtraRuleType.DirectoryName,
- Token = "extras",
- MediaType = MediaType.Video,
- },
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Filename,
+ "trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Suffix,
+ "-trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Suffix,
+ ".trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Suffix,
+ "_trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
+ ExtraRuleType.Suffix,
+ " trailer",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Filename,
+ "sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Suffix,
+ "-sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Suffix,
+ ".sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Suffix,
+ "_sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.Suffix,
+ " sample",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.Filename,
+ "theme",
+ MediaType.Audio),
+
+ new ExtraRule(
+ ExtraType.Scene,
+ ExtraRuleType.Suffix,
+ "-scene",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.Suffix,
+ "-clip",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Interview,
+ ExtraRuleType.Suffix,
+ "-interview",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.BehindTheScenes,
+ ExtraRuleType.Suffix,
+ "-behindthescenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.Suffix,
+ "-deleted",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.Suffix,
+ "-featurette",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.Suffix,
+ "-short",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.BehindTheScenes,
+ ExtraRuleType.DirectoryName,
+ "behind the scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.DirectoryName,
+ "deleted scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Interview,
+ ExtraRuleType.DirectoryName,
+ "interviews",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Scene,
+ ExtraRuleType.DirectoryName,
+ "scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.DirectoryName,
+ "samples",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.DirectoryName,
+ "shorts",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.DirectoryName,
+ "featurettes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Unknown,
+ ExtraRuleType.DirectoryName,
+ "extras",
+ MediaType.Video),
};
Format3DRules = new[]
{
// Kodi rules:
- new Format3DRule
- {
- PreceedingToken = "3d",
- Token = "hsbs"
- },
- new Format3DRule
- {
- PreceedingToken = "3d",
- Token = "sbs"
- },
- new Format3DRule
- {
- PreceedingToken = "3d",
- Token = "htab"
- },
- new Format3DRule
- {
- PreceedingToken = "3d",
- Token = "tab"
- },
- // Media Browser rules:
- new Format3DRule
- {
- Token = "fsbs"
- },
- new Format3DRule
- {
- Token = "hsbs"
- },
- new Format3DRule
- {
- Token = "sbs"
- },
- new Format3DRule
- {
- Token = "ftab"
- },
- new Format3DRule
- {
- Token = "htab"
- },
- new Format3DRule
- {
- Token = "tab"
- },
- new Format3DRule
- {
- Token = "sbs3d"
- },
- new Format3DRule
- {
- Token = "mvc"
- }
+ new Format3DRule(
+ precedingToken: "3d",
+ token: "hsbs"),
+
+ new Format3DRule(
+ precedingToken: "3d",
+ token: "sbs"),
+
+ new Format3DRule(
+ precedingToken: "3d",
+ token: "htab"),
+
+ new Format3DRule(
+ precedingToken: "3d",
+ token: "tab"),
+
+ // Media Browser rules:
+ new Format3DRule("fsbs"),
+ new Format3DRule("hsbs"),
+ new Format3DRule("sbs"),
+ new Format3DRule("ftab"),
+ new Format3DRule("htab"),
+ new Format3DRule("tab"),
+ new Format3DRule("sbs3d"),
+ new Format3DRule("mvc")
};
+
AudioBookPartsExpressions = new[]
{
// Detect specified chapters, like CH 01
@@ -631,13 +577,20 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?<chapter>[0-9]+)",
// Part if often ending of filename
- "(?<part>[0-9]+)$",
+ @"(?<!ch(?:apter) )(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
};
+ AudioBookNamesExpressions = new[]
+ {
+ // Detect year usually in brackets after name Batman (2020)
+ @"^(?<name>.+?)\s*\(\s*(?<year>\d{4})\s*\)\s*$",
+ @"^\s*(?<name>[^ ].*?)\s*$"
+ };
+
var extensions = VideoFileExtensions.ToList();
extensions.AddRange(new[]
@@ -673,7 +626,7 @@ namespace Emby.Naming.Common
".mxf"
});
- MultipleEpisodeExpressions = new string[]
+ MultipleEpisodeExpressions = new[]
{
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@@ -697,56 +650,139 @@ namespace Emby.Naming.Common
Compile();
}
+ /// <summary>
+ /// Gets or sets list of audio file extensions.
+ /// </summary>
public string[] AudioFileExtensions { get; set; }
+ /// <summary>
+ /// Gets or sets list of album stacking prefixes.
+ /// </summary>
public string[] AlbumStackingPrefixes { get; set; }
+ /// <summary>
+ /// Gets or sets list of subtitle file extensions.
+ /// </summary>
public string[] SubtitleFileExtensions { get; set; }
+ /// <summary>
+ /// Gets or sets list of subtitles flag delimiters.
+ /// </summary>
public char[] SubtitleFlagDelimiters { get; set; }
+ /// <summary>
+ /// Gets or sets list of subtitle forced flags.
+ /// </summary>
public string[] SubtitleForcedFlags { get; set; }
+ /// <summary>
+ /// Gets or sets list of subtitle default flags.
+ /// </summary>
public string[] SubtitleDefaultFlags { get; set; }
+ /// <summary>
+ /// Gets or sets list of episode regular expressions.
+ /// </summary>
public EpisodeExpression[] EpisodeExpressions { get; set; }
+ /// <summary>
+ /// Gets or sets list of raw episode without season regular expressions strings.
+ /// </summary>
public string[] EpisodeWithoutSeasonExpressions { get; set; }
+ /// <summary>
+ /// Gets or sets list of raw multi-part episodes regular expressions strings.
+ /// </summary>
public string[] EpisodeMultiPartExpressions { get; set; }
+ /// <summary>
+ /// Gets or sets list of video file extensions.
+ /// </summary>
public string[] VideoFileExtensions { get; set; }
+ /// <summary>
+ /// Gets or sets list of video stub file extensions.
+ /// </summary>
public string[] StubFileExtensions { get; set; }
+ /// <summary>
+ /// Gets or sets list of raw audiobook parts regular expressions strings.
+ /// </summary>
public string[] AudioBookPartsExpressions { get; set; }
+ /// <summary>
+ /// Gets or sets list of raw audiobook names regular expressions strings.
+ /// </summary>
+ public string[] AudioBookNamesExpressions { get; set; }
+
+ /// <summary>
+ /// Gets or sets list of stub type rules.
+ /// </summary>
public StubTypeRule[] StubTypes { get; set; }
+ /// <summary>
+ /// Gets or sets list of video flag delimiters.
+ /// </summary>
public char[] VideoFlagDelimiters { get; set; }
+ /// <summary>
+ /// Gets or sets list of 3D Format rules.
+ /// </summary>
public Format3DRule[] Format3DRules { get; set; }
+ /// <summary>
+ /// Gets or sets list of raw video file-stacking expressions strings.
+ /// </summary>
public string[] VideoFileStackingExpressions { get; set; }
+ /// <summary>
+ /// Gets or sets list of raw clean DateTimes regular expressions strings.
+ /// </summary>
public string[] CleanDateTimes { get; set; }
+ /// <summary>
+ /// Gets or sets list of raw clean strings regular expressions strings.
+ /// </summary>
public string[] CleanStrings { get; set; }
+ /// <summary>
+ /// Gets or sets list of multi-episode regular expressions.
+ /// </summary>
public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
+ /// <summary>
+ /// Gets or sets list of extra rules for videos.
+ /// </summary>
public ExtraRule[] VideoExtraRules { get; set; }
- public Regex[] VideoFileStackingRegexes { get; private set; }
-
- public Regex[] CleanDateTimeRegexes { get; private set; }
-
- public Regex[] CleanStringRegexes { get; private set; }
-
- public Regex[] EpisodeWithoutSeasonRegexes { get; private set; }
-
- public Regex[] EpisodeMultiPartRegexes { get; private set; }
-
+ /// <summary>
+ /// Gets list of video file-stack regular expressions.
+ /// </summary>
+ public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
+
+ /// <summary>
+ /// Gets list of clean datetime regular expressions.
+ /// </summary>
+ public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
+
+ /// <summary>
+ /// Gets list of clean string regular expressions.
+ /// </summary>
+ public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
+
+ /// <summary>
+ /// Gets list of episode without season regular expressions.
+ /// </summary>
+ public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
+
+ /// <summary>
+ /// Gets list of multi-part episode regular expressions.
+ /// </summary>
+ public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
+
+ /// <summary>
+ /// Compiles raw regex strings into regexes.
+ /// </summary>
public void Compile()
{
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 6857f9952..24c15759d 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -14,6 +14,7 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
+ <Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@@ -38,7 +39,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->
diff --git a/Emby.Naming/Subtitles/SubtitleInfo.cs b/Emby.Naming/Subtitles/SubtitleInfo.cs
index f39c496b7..1fb2e0dc8 100644
--- a/Emby.Naming/Subtitles/SubtitleInfo.cs
+++ b/Emby.Naming/Subtitles/SubtitleInfo.cs
@@ -1,10 +1,24 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Subtitles
{
+ /// <summary>
+ /// Class holding information about subtitle.
+ /// </summary>
public class SubtitleInfo
{
/// <summary>
+ /// Initializes a new instance of the <see cref="SubtitleInfo"/> class.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <param name="isDefault">Is subtitle default.</param>
+ /// <param name="isForced">Is subtitle forced.</param>
+ public SubtitleInfo(string path, bool isDefault, bool isForced)
+ {
+ Path = path;
+ IsDefault = isDefault;
+ IsForced = isForced;
+ }
+
+ /// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
@@ -14,7 +28,7 @@ namespace Emby.Naming.Subtitles
/// Gets or sets the language.
/// </summary>
/// <value>The language.</value>
- public string Language { get; set; }
+ public string? Language { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is default.
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
index 24e59f90a..a19340ef6 100644
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ b/Emby.Naming/Subtitles/SubtitleParser.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -8,20 +5,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.Subtitles
{
+ /// <summary>
+ /// Subtitle Parser class.
+ /// </summary>
public class SubtitleParser
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SubtitleParser"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param>
public SubtitleParser(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns>
public SubtitleInfo? ParseFile(string path)
{
if (path.Length == 0)
{
- throw new ArgumentException("File path can't be empty.", nameof(path));
+ return null;
}
var extension = Path.GetExtension(path);
@@ -31,12 +40,10 @@ namespace Emby.Naming.Subtitles
}
var flags = GetFlags(path);
- var info = new SubtitleInfo
- {
- Path = path,
- IsDefault = _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
- IsForced = _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase))
- };
+ var info = new SubtitleInfo(
+ path,
+ _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
+ _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
@@ -53,7 +60,7 @@ namespace Emby.Naming.Subtitles
private string[] GetFlags(string path)
{
- // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
+ // Note: the tags need be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path);
diff --git a/Emby.Naming/TV/EpisodeInfo.cs b/Emby.Naming/TV/EpisodeInfo.cs
index 250df4e2d..a8920b36a 100644
--- a/Emby.Naming/TV/EpisodeInfo.cs
+++ b/Emby.Naming/TV/EpisodeInfo.cs
@@ -1,10 +1,20 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.TV
{
+ /// <summary>
+ /// Holder object for Episode information.
+ /// </summary>
public class EpisodeInfo
{
/// <summary>
+ /// Initializes a new instance of the <see cref="EpisodeInfo"/> class.
+ /// </summary>
+ /// <param name="path">Path to the file.</param>
+ public EpisodeInfo(string path)
+ {
+ Path = path;
+ }
+
+ /// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
@@ -14,19 +24,19 @@ namespace Emby.Naming.TV
/// Gets or sets the container.
/// </summary>
/// <value>The container.</value>
- public string Container { get; set; }
+ public string? Container { get; set; }
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
- public string SeriesName { get; set; }
+ public string? SeriesName { get; set; }
/// <summary>
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
- public string Format3D { get; set; }
+ public string? Format3D { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [is3 d].
@@ -44,20 +54,41 @@ namespace Emby.Naming.TV
/// Gets or sets the type of the stub.
/// </summary>
/// <value>The type of the stub.</value>
- public string StubType { get; set; }
+ public string? StubType { get; set; }
+ /// <summary>
+ /// Gets or sets optional season number.
+ /// </summary>
public int? SeasonNumber { get; set; }
+ /// <summary>
+ /// Gets or sets optional episode number.
+ /// </summary>
public int? EpisodeNumber { get; set; }
- public int? EndingEpsiodeNumber { get; set; }
+ /// <summary>
+ /// Gets or sets optional ending episode number. For multi-episode files 1-13.
+ /// </summary>
+ public int? EndingEpisodeNumber { get; set; }
+ /// <summary>
+ /// Gets or sets optional year of release.
+ /// </summary>
public int? Year { get; set; }
+ /// <summary>
+ /// Gets or sets optional year of release.
+ /// </summary>
public int? Month { get; set; }
+ /// <summary>
+ /// Gets or sets optional day of release.
+ /// </summary>
public int? Day { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether by date expression was used.
+ /// </summary>
public bool IsByDate { get; set; }
}
}
diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs
index a6af689c7..6d0597356 100644
--- a/Emby.Naming/TV/EpisodePathParser.cs
+++ b/Emby.Naming/TV/EpisodePathParser.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -9,15 +6,32 @@ using Emby.Naming.Common;
namespace Emby.Naming.TV
{
+ /// <summary>
+ /// Used to parse information about episode from path.
+ /// </summary>
public class EpisodePathParser
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpisodePathParser"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
public EpisodePathParser(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Parses information about episode from path.
+ /// </summary>
+ /// <param name="path">Path.</param>
+ /// <param name="isDirectory">Is path for a directory or file.</param>
+ /// <param name="isNamed">Do we want to use IsNamed expressions.</param>
+ /// <param name="isOptimistic">Do we want to use Optimistic expressions.</param>
+ /// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</param>
+ /// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param>
+ /// <returns>Returns <see cref="EpisodePathParserResult"/> object.</returns>
public EpisodePathParserResult Parse(
string path,
bool isDirectory,
@@ -146,7 +160,7 @@ namespace Emby.Naming.TV
{
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
- result.EndingEpsiodeNumber = num;
+ result.EndingEpisodeNumber = num;
}
}
}
@@ -186,7 +200,7 @@ namespace Emby.Naming.TV
private void FillAdditional(string path, EpisodePathParserResult info)
{
- var expressions = _options.MultipleEpisodeExpressions.ToList();
+ var expressions = _options.MultipleEpisodeExpressions.Where(i => i.IsNamed).ToList();
if (string.IsNullOrEmpty(info.SeriesName))
{
@@ -200,11 +214,6 @@ namespace Emby.Naming.TV
{
foreach (var i in expressions)
{
- if (!i.IsNamed)
- {
- continue;
- }
-
var result = Parse(path, i);
if (!result.Success)
@@ -217,13 +226,13 @@ namespace Emby.Naming.TV
info.SeriesName = result.SeriesName;
}
- if (!info.EndingEpsiodeNumber.HasValue && info.EpisodeNumber.HasValue)
+ if (!info.EndingEpisodeNumber.HasValue && info.EpisodeNumber.HasValue)
{
- info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
+ info.EndingEpisodeNumber = result.EndingEpisodeNumber;
}
if (!string.IsNullOrEmpty(info.SeriesName)
- && (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
+ && (!info.EpisodeNumber.HasValue || info.EndingEpisodeNumber.HasValue))
{
break;
}
diff --git a/Emby.Naming/TV/EpisodePathParserResult.cs b/Emby.Naming/TV/EpisodePathParserResult.cs
index 05f921edc..233d5a4f6 100644
--- a/Emby.Naming/TV/EpisodePathParserResult.cs
+++ b/Emby.Naming/TV/EpisodePathParserResult.cs
@@ -1,25 +1,54 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.TV
{
+ /// <summary>
+ /// Holder object for <see cref="EpisodePathParser"/> result.
+ /// </summary>
public class EpisodePathParserResult
{
+ /// <summary>
+ /// Gets or sets optional season number.
+ /// </summary>
public int? SeasonNumber { get; set; }
+ /// <summary>
+ /// Gets or sets optional episode number.
+ /// </summary>
public int? EpisodeNumber { get; set; }
- public int? EndingEpsiodeNumber { get; set; }
+ /// <summary>
+ /// Gets or sets optional ending episode number. For multi-episode files 1-13.
+ /// </summary>
+ public int? EndingEpisodeNumber { get; set; }
- public string SeriesName { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the series.
+ /// </summary>
+ /// <value>The name of the series.</value>
+ public string? SeriesName { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether parsing was successful.
+ /// </summary>
public bool Success { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether by date expression was used.
+ /// </summary>
public bool IsByDate { get; set; }
+ /// <summary>
+ /// Gets or sets optional year of release.
+ /// </summary>
public int? Year { get; set; }
+ /// <summary>
+ /// Gets or sets optional year of release.
+ /// </summary>
public int? Month { get; set; }
+ /// <summary>
+ /// Gets or sets optional day of release.
+ /// </summary>
public int? Day { get; set; }
}
}
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index 6994f69fc..f7df58786 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
@@ -9,15 +6,32 @@ using Emby.Naming.Video;
namespace Emby.Naming.TV
{
+ /// <summary>
+ /// Used to resolve information about episode from path.
+ /// </summary>
public class EpisodeResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
public EpisodeResolver(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Resolve information about episode from path.
+ /// </summary>
+ /// <param name="path">Path.</param>
+ /// <param name="isDirectory">Is path for a directory or file.</param>
+ /// <param name="isNamed">Do we want to use IsNamed expressions.</param>
+ /// <param name="isOptimistic">Do we want to use Optimistic expressions.</param>
+ /// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</param>
+ /// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param>
+ /// <returns>Returns null or <see cref="EpisodeInfo"/> object if successful.</returns>
public EpisodeInfo? Resolve(
string path,
bool isDirectory,
@@ -54,12 +68,11 @@ namespace Emby.Naming.TV
var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
- return new EpisodeInfo
+ return new EpisodeInfo(path)
{
- Path = path,
Container = container,
IsStub = isStub,
- EndingEpsiodeNumber = parsingResult.EndingEpsiodeNumber,
+ EndingEpisodeNumber = parsingResult.EndingEpisodeNumber,
EpisodeNumber = parsingResult.EpisodeNumber,
SeasonNumber = parsingResult.SeasonNumber,
SeriesName = parsingResult.SeriesName,
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index d2e324dda..d11c7c99e 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -1,11 +1,12 @@
-#pragma warning disable CS1591
-
using System;
using System.Globalization;
using System.IO;
namespace Emby.Naming.TV
{
+ /// <summary>
+ /// Class to parse season paths.
+ /// </summary>
public static class SeasonPathParser
{
/// <summary>
@@ -23,6 +24,13 @@ namespace Emby.Naming.TV
"stagione"
};
+ /// <summary>
+ /// Attempts to parse season number from path.
+ /// </summary>
+ /// <param name="path">Path to season.</param>
+ /// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
+ /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
+ /// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{
var result = new SeasonPathParserResult();
@@ -101,9 +109,9 @@ namespace Emby.Naming.TV
}
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
- for (int i = 0; i < parts.Length; i++)
+ foreach (var part in parts)
{
- if (TryGetSeasonNumberFromPart(parts[i], out int seasonNumber))
+ if (TryGetSeasonNumberFromPart(part, out int seasonNumber))
{
return (seasonNumber, true);
}
@@ -139,7 +147,7 @@ namespace Emby.Naming.TV
var numericStart = -1;
var length = 0;
- var hasOpenParenth = false;
+ var hasOpenParenthesis = false;
var isSeasonFolder = true;
// Find out where the numbers start, and then keep going until they end
@@ -147,7 +155,7 @@ namespace Emby.Naming.TV
{
if (char.IsNumber(path[i]))
{
- if (!hasOpenParenth)
+ if (!hasOpenParenthesis)
{
if (numericStart == -1)
{
@@ -167,11 +175,11 @@ namespace Emby.Naming.TV
var currentChar = path[i];
if (currentChar == '(')
{
- hasOpenParenth = true;
+ hasOpenParenthesis = true;
}
else if (currentChar == ')')
{
- hasOpenParenth = false;
+ hasOpenParenthesis = false;
}
}
diff --git a/Emby.Naming/TV/SeasonPathParserResult.cs b/Emby.Naming/TV/SeasonPathParserResult.cs
index a142fafea..b4b6f236a 100644
--- a/Emby.Naming/TV/SeasonPathParserResult.cs
+++ b/Emby.Naming/TV/SeasonPathParserResult.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.TV
{
+ /// <summary>
+ /// Data object to pass result of <see cref="SeasonPathParser"/>.
+ /// </summary>
public class SeasonPathParserResult
{
/// <summary>
@@ -16,6 +17,10 @@ namespace Emby.Naming.TV
/// <value><c>true</c> if success; otherwise, <c>false</c>.</value>
public bool Success { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether "Is season folder".
+ /// Seems redundant and barely used.
+ /// </summary>
public bool IsSeasonFolder { get; set; }
}
}
diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs
index f05d540f8..0ee633dcc 100644
--- a/Emby.Naming/Video/CleanDateTimeParser.cs
+++ b/Emby.Naming/Video/CleanDateTimeParser.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
@@ -12,6 +9,12 @@ namespace Emby.Naming.Video
/// </summary>
public static class CleanDateTimeParser
{
+ /// <summary>
+ /// Attempts to clean the name.
+ /// </summary>
+ /// <param name="name">Name of video.</param>
+ /// <param name="cleanDateTimeRegexes">Optional list of regexes to clean the name.</param>
+ /// <returns>Returns <see cref="CleanDateTimeResult"/> object.</returns>
public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
{
CleanDateTimeResult result = new CleanDateTimeResult(name);
diff --git a/Emby.Naming/Video/CleanDateTimeResult.cs b/Emby.Naming/Video/CleanDateTimeResult.cs
index 57eeaa7e3..c675a19d0 100644
--- a/Emby.Naming/Video/CleanDateTimeResult.cs
+++ b/Emby.Naming/Video/CleanDateTimeResult.cs
@@ -1,22 +1,21 @@
-#pragma warning disable CS1591
-#nullable enable
-
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Holder structure for name and year.
+ /// </summary>
public readonly struct CleanDateTimeResult
{
- public CleanDateTimeResult(string name, int? year)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CleanDateTimeResult"/> struct.
+ /// </summary>
+ /// <param name="name">Name of video.</param>
+ /// <param name="year">Year of release.</param>
+ public CleanDateTimeResult(string name, int? year = null)
{
Name = name;
Year = year;
}
- public CleanDateTimeResult(string name)
- {
- Name = name;
- Year = null;
- }
-
/// <summary>
/// Gets the name.
/// </summary>
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index 3f584d584..09a0cd189 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@@ -12,6 +9,13 @@ namespace Emby.Naming.Video
/// </summary>
public static class CleanStringParser
{
+ /// <summary>
+ /// Attempts to extract clean name with regular expressions.
+ /// </summary>
+ /// <param name="name">Name of file.</param>
+ /// <param name="expressions">List of regex to parse name and year from.</param>
+ /// <param name="newName">Parsing result string.</param>
+ /// <returns>True if parsing was successful.</returns>
public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{
var len = expressions.Count;
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index fc0424faa..1d3b36a1a 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -9,15 +7,27 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Resolve if file is extra for video.
+ /// </summary>
public class ExtraResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExtraResolver"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
public ExtraResolver(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Attempts to resolve if file is extra.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public ExtraResult GetExtraInfo(string path)
{
return _options.VideoExtraRules
@@ -43,10 +53,6 @@ namespace Emby.Naming.Video
return result;
}
}
- else
- {
- return result;
- }
if (rule.RuleType == ExtraRuleType.Filename)
{
diff --git a/Emby.Naming/Video/ExtraResult.cs b/Emby.Naming/Video/ExtraResult.cs
index 15db32e87..243fc2b41 100644
--- a/Emby.Naming/Video/ExtraResult.cs
+++ b/Emby.Naming/Video/ExtraResult.cs
@@ -1,9 +1,10 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Holder object for passing results from ExtraResolver.
+ /// </summary>
public class ExtraResult
{
/// <summary>
@@ -16,6 +17,6 @@ namespace Emby.Naming.Video
/// Gets or sets the rule.
/// </summary>
/// <value>The rule.</value>
- public ExtraRule Rule { get; set; }
+ public ExtraRule? Rule { get; set; }
}
}
diff --git a/Emby.Naming/Video/ExtraRule.cs b/Emby.Naming/Video/ExtraRule.cs
index 7c9702e24..e267ac55f 100644
--- a/Emby.Naming/Video/ExtraRule.cs
+++ b/Emby.Naming/Video/ExtraRule.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Model.Entities;
using MediaType = Emby.Naming.Common.MediaType;
@@ -11,6 +9,21 @@ namespace Emby.Naming.Video
public class ExtraRule
{
/// <summary>
+ /// Initializes a new instance of the <see cref="ExtraRule"/> class.
+ /// </summary>
+ /// <param name="extraType">Type of extra.</param>
+ /// <param name="ruleType">Type of rule.</param>
+ /// <param name="token">Token.</param>
+ /// <param name="mediaType">Media type.</param>
+ public ExtraRule(ExtraType extraType, ExtraRuleType ruleType, string token, MediaType mediaType)
+ {
+ Token = token;
+ ExtraType = extraType;
+ RuleType = ruleType;
+ MediaType = mediaType;
+ }
+
+ /// <summary>
/// Gets or sets the token to use for matching against the file path.
/// </summary>
public string Token { get; set; }
diff --git a/Emby.Naming/Video/ExtraRuleType.cs b/Emby.Naming/Video/ExtraRuleType.cs
index e89876f4a..324319505 100644
--- a/Emby.Naming/Video/ExtraRuleType.cs
+++ b/Emby.Naming/Video/ExtraRuleType.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Extra rules type to determine against what <see cref="ExtraRule.Token"/> should be matched.
+ /// </summary>
public enum ExtraRuleType
{
/// <summary>
@@ -22,6 +23,6 @@ namespace Emby.Naming.Video
/// <summary>
/// Match <see cref="ExtraRule.Token"/> against the name of the directory containing the file.
/// </summary>
- DirectoryName = 3,
+ DirectoryName = 3
}
}
diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs
index 3ef190b86..6519db57c 100644
--- a/Emby.Naming/Video/FileStack.cs
+++ b/Emby.Naming/Video/FileStack.cs
@@ -1,24 +1,43 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Object holding list of files paths with additional information.
+ /// </summary>
public class FileStack
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FileStack"/> class.
+ /// </summary>
public FileStack()
{
Files = new List<string>();
}
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets name of file stack.
+ /// </summary>
+ public string Name { get; set; } = string.Empty;
+ /// <summary>
+ /// Gets or sets list of paths in stack.
+ /// </summary>
public List<string> Files { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether stack is directory stack.
+ /// </summary>
public bool IsDirectoryStack { get; set; }
+ /// <summary>
+ /// Helper function to determine if path is in the stack.
+ /// </summary>
+ /// <param name="file">Path of desired file.</param>
+ /// <param name="isDirectory">Requested type of stack.</param>
+ /// <returns>True if file is in the stack.</returns>
public bool ContainsFile(string file, bool isDirectory)
{
if (IsDirectoryStack == isDirectory)
diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs
index a8bd9d5c5..439de1813 100644
--- a/Emby.Naming/Video/FlagParser.cs
+++ b/Emby.Naming/Video/FlagParser.cs
@@ -1,37 +1,53 @@
-#pragma warning disable CS1591
-
using System;
using System.IO;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Parses list of flags from filename based on delimiters.
+ /// </summary>
public class FlagParser
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FlagParser"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
public FlagParser(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <returns>List of found flags.</returns>
public string[] GetFlags(string path)
{
return GetFlags(path, _options.VideoFlagDelimiters);
}
- public string[] GetFlags(string path, char[] delimeters)
+ /// <summary>
+ /// Parses flags from filename based on delimiters.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <param name="delimiters">Delimiters used to extract flags.</param>
+ /// <returns>List of found flags.</returns>
+ public string[] GetFlags(string path, char[] delimiters)
{
if (string.IsNullOrEmpty(path))
{
- throw new ArgumentNullException(nameof(path));
+ return Array.Empty<string>();
}
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path);
- return file.Split(delimeters, StringSplitOptions.RemoveEmptyEntries);
+ return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
}
}
}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 51c26af86..4fd5d78ba 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/Emby.Naming/Video/Format3DParser.cs
@@ -1,28 +1,38 @@
-#pragma warning disable CS1591
-
using System;
using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Parste 3D format related flags.
+ /// </summary>
public class Format3DParser
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Format3DParser"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
public Format3DParser(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Parse 3D format related flags.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <returns>Returns <see cref="Format3DResult"/> object.</returns>
public Format3DResult Parse(string path)
{
int oldLen = _options.VideoFlagDelimiters.Length;
- var delimeters = new char[oldLen + 1];
- _options.VideoFlagDelimiters.CopyTo(delimeters, 0);
- delimeters[oldLen] = ' ';
+ var delimiters = new char[oldLen + 1];
+ _options.VideoFlagDelimiters.CopyTo(delimiters, 0);
+ delimiters[oldLen] = ' ';
- return Parse(new FlagParser(_options).GetFlags(path, delimeters));
+ return Parse(new FlagParser(_options).GetFlags(path, delimiters));
}
internal Format3DResult Parse(string[] videoFlags)
@@ -44,7 +54,7 @@ namespace Emby.Naming.Video
{
var result = new Format3DResult();
- if (string.IsNullOrEmpty(rule.PreceedingToken))
+ if (string.IsNullOrEmpty(rule.PrecedingToken))
{
result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
result.Is3D = !string.IsNullOrEmpty(result.Format3D);
@@ -57,13 +67,13 @@ namespace Emby.Naming.Video
else
{
var foundPrefix = false;
- string format = null;
+ string? format = null;
foreach (var flag in videoFlags)
{
if (foundPrefix)
{
- result.Tokens.Add(rule.PreceedingToken);
+ result.Tokens.Add(rule.PrecedingToken);
if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
{
@@ -74,7 +84,7 @@ namespace Emby.Naming.Video
break;
}
- foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase);
+ foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
}
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs
index fa0e9d3b8..ac935f203 100644
--- a/Emby.Naming/Video/Format3DResult.cs
+++ b/Emby.Naming/Video/Format3DResult.cs
@@ -1,11 +1,15 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Helper object to return data from <see cref="Format3DParser"/>.
+ /// </summary>
public class Format3DResult
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Format3DResult"/> class.
+ /// </summary>
public Format3DResult()
{
Tokens = new List<string>();
@@ -21,7 +25,7 @@ namespace Emby.Naming.Video
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
- public string Format3D { get; set; }
+ public string? Format3D { get; set; }
/// <summary>
/// Gets or sets the tokens.
diff --git a/Emby.Naming/Video/Format3DRule.cs b/Emby.Naming/Video/Format3DRule.cs
index 310ec84e8..e562691df 100644
--- a/Emby.Naming/Video/Format3DRule.cs
+++ b/Emby.Naming/Video/Format3DRule.cs
@@ -1,19 +1,31 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Data holder class for 3D format rule.
+ /// </summary>
public class Format3DRule
{
/// <summary>
+ /// Initializes a new instance of the <see cref="Format3DRule"/> class.
+ /// </summary>
+ /// <param name="token">Token.</param>
+ /// <param name="precedingToken">Token present before current token.</param>
+ public Format3DRule(string token, string? precedingToken = null)
+ {
+ Token = token;
+ PrecedingToken = precedingToken;
+ }
+
+ /// <summary>
/// Gets or sets the token.
/// </summary>
/// <value>The token.</value>
public string Token { get; set; }
/// <summary>
- /// Gets or sets the preceeding token.
+ /// Gets or sets the preceding token.
/// </summary>
- /// <value>The preceeding token.</value>
- public string PreceedingToken { get; set; }
+ /// <value>The preceding token.</value>
+ public string? PrecedingToken { get; set; }
}
}
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index f733cd262..550c42961 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -1,58 +1,88 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Resolve <see cref="FileStack"/> from list of paths.
+ /// </summary>
public class StackResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StackResolver"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
public StackResolver(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Resolves only directories from paths.
+ /// </summary>
+ /// <param name="files">List of paths.</param>
+ /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
}
+ /// <summary>
+ /// Resolves only files from paths.
+ /// </summary>
+ /// <param name="files">List of paths.</param>
+ /// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
{
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
}
- public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<FileSystemMetadata> files)
+ /// <summary>
+ /// Resolves audiobooks from paths.
+ /// </summary>
+ /// <param name="files">List of paths.</param>
+ /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
+ public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
{
- var groupedDirectoryFiles = files.GroupBy(file =>
- file.IsDirectory
- ? file.FullName
- : Path.GetDirectoryName(file.FullName));
+ var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
foreach (var directory in groupedDirectoryFiles)
{
- var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
- foreach (var file in directory)
+ if (string.IsNullOrEmpty(directory.Key))
{
- if (file.IsDirectory)
+ foreach (var file in directory)
{
- continue;
+ var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
+ stack.Files.Add(file.Path);
+ yield return stack;
}
-
- stack.Files.Add(file.FullName);
}
+ else
+ {
+ var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
+ foreach (var file in directory)
+ {
+ stack.Files.Add(file.Path);
+ }
- yield return stack;
+ yield return stack;
+ }
}
}
+ /// <summary>
+ /// Resolves videos from paths.
+ /// </summary>
+ /// <param name="files">List of paths.</param>
+ /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{
var resolver = new VideoResolver(_options);
@@ -81,10 +111,10 @@ namespace Emby.Naming.Video
if (match1.Success)
{
- var title1 = match1.Groups[1].Value;
- var volume1 = match1.Groups[2].Value;
- var ignore1 = match1.Groups[3].Value;
- var extension1 = match1.Groups[4].Value;
+ var title1 = match1.Groups["title"].Value;
+ var volume1 = match1.Groups["volume"].Value;
+ var ignore1 = match1.Groups["ignore"].Value;
+ var extension1 = match1.Groups["extension"].Value;
var j = i + 1;
while (j < list.Count)
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index f1b5d7bcc..079987fe8 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
@@ -8,13 +5,23 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Resolve if file is stub (.disc).
+ /// </summary>
public static class StubResolver
{
+ /// <summary>
+ /// Tries to resolve if file is stub (.disc).
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <param name="options">NamingOptions containing StubFileExtensions and StubTypes.</param>
+ /// <param name="stubType">Stub type.</param>
+ /// <returns>True if file is a stub.</returns>
public static bool TryResolveFile(string path, NamingOptions options, out string? stubType)
{
stubType = default;
- if (path == null)
+ if (string.IsNullOrEmpty(path))
{
return false;
}
diff --git a/Emby.Naming/Video/StubResult.cs b/Emby.Naming/Video/StubResult.cs
deleted file mode 100644
index 1b8e99b0d..000000000
--- a/Emby.Naming/Video/StubResult.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Naming.Video
-{
- public struct StubResult
- {
- /// <summary>
- /// Gets or sets a value indicating whether this instance is stub.
- /// </summary>
- /// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
- public bool IsStub { get; set; }
-
- /// <summary>
- /// Gets or sets the type of the stub.
- /// </summary>
- /// <value>The type of the stub.</value>
- public string StubType { get; set; }
- }
-}
diff --git a/Emby.Naming/Video/StubTypeRule.cs b/Emby.Naming/Video/StubTypeRule.cs
index 8285cb51a..dfb3ac013 100644
--- a/Emby.Naming/Video/StubTypeRule.cs
+++ b/Emby.Naming/Video/StubTypeRule.cs
@@ -1,10 +1,22 @@
-#pragma warning disable CS1591
-
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Data class holding information about Stub type rule.
+ /// </summary>
public class StubTypeRule
{
/// <summary>
+ /// Initializes a new instance of the <see cref="StubTypeRule"/> class.
+ /// </summary>
+ /// <param name="token">Token.</param>
+ /// <param name="stubType">Stub type.</param>
+ public StubTypeRule(string token, string stubType)
+ {
+ Token = token;
+ StubType = stubType;
+ }
+
+ /// <summary>
/// Gets or sets the token.
/// </summary>
/// <value>The token.</value>
diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs
index 11e789b66..1457db737 100644
--- a/Emby.Naming/Video/VideoFileInfo.cs
+++ b/Emby.Naming/Video/VideoFileInfo.cs
@@ -8,6 +8,35 @@ namespace Emby.Naming.Video
public class VideoFileInfo
{
/// <summary>
+ /// Initializes a new instance of the <see cref="VideoFileInfo"/> class.
+ /// </summary>
+ /// <param name="name">Name of file.</param>
+ /// <param name="path">Path to the file.</param>
+ /// <param name="container">Container type.</param>
+ /// <param name="year">Year of release.</param>
+ /// <param name="extraType">Extra type.</param>
+ /// <param name="extraRule">Extra rule.</param>
+ /// <param name="format3D">Format 3D.</param>
+ /// <param name="is3D">Is 3D.</param>
+ /// <param name="isStub">Is Stub.</param>
+ /// <param name="stubType">Stub type.</param>
+ /// <param name="isDirectory">Is directory.</param>
+ public VideoFileInfo(string name, string path, string? container, int? year = default, ExtraType? extraType = default, ExtraRule? extraRule = default, string? format3D = default, bool is3D = default, bool isStub = default, string? stubType = default, bool isDirectory = default)
+ {
+ Path = path;
+ Container = container;
+ Name = name;
+ Year = year;
+ ExtraType = extraType;
+ ExtraRule = extraRule;
+ Format3D = format3D;
+ Is3D = is3D;
+ IsStub = isStub;
+ StubType = stubType;
+ IsDirectory = isDirectory;
+ }
+
+ /// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
@@ -17,7 +46,7 @@ namespace Emby.Naming.Video
/// Gets or sets the container.
/// </summary>
/// <value>The container.</value>
- public string Container { get; set; }
+ public string? Container { get; set; }
/// <summary>
/// Gets or sets the name.
@@ -41,13 +70,13 @@ namespace Emby.Naming.Video
/// Gets or sets the extra rule.
/// </summary>
/// <value>The extra rule.</value>
- public ExtraRule ExtraRule { get; set; }
+ public ExtraRule? ExtraRule { get; set; }
/// <summary>
/// Gets or sets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
- public string Format3D { get; set; }
+ public string? Format3D { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [is3 d].
@@ -65,7 +94,7 @@ namespace Emby.Naming.Video
/// Gets or sets the type of the stub.
/// </summary>
/// <value>The type of the stub.</value>
- public string StubType { get; set; }
+ public string? StubType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is a directory.
@@ -84,8 +113,7 @@ namespace Emby.Naming.Video
/// <inheritdoc />
public override string ToString()
{
- // Makes debugging easier
- return Name ?? base.ToString();
+ return "VideoFileInfo(Name: '" + Name + "')";
}
}
}
diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs
index ea74c40e2..930fdb33f 100644
--- a/Emby.Naming/Video/VideoInfo.cs
+++ b/Emby.Naming/Video/VideoInfo.cs
@@ -12,7 +12,7 @@ namespace Emby.Naming.Video
/// Initializes a new instance of the <see cref="VideoInfo" /> class.
/// </summary>
/// <param name="name">The name.</param>
- public VideoInfo(string name)
+ public VideoInfo(string? name)
{
Name = name;
@@ -25,7 +25,7 @@ namespace Emby.Naming.Video
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the year.
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 948fe037b..5f83355c8 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -11,22 +9,35 @@ using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Resolves alternative versions and extras from list of video files.
+ /// </summary>
public class VideoListResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoListResolver"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
public VideoListResolver(NamingOptions options)
{
_options = options;
}
+ /// <summary>
+ /// Resolves alternative versions and extras from list of video files.
+ /// </summary>
+ /// <param name="files">List of related video files.</param>
+ /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
+ /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
{
var videoResolver = new VideoResolver(_options);
var videoInfos = files
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
- .Where(i => i != null)
+ .OfType<VideoFileInfo>()
.ToList();
// Filter out all extras, otherwise they could cause stacks to not be resolved
@@ -39,7 +50,7 @@ namespace Emby.Naming.Video
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos
- .Where(i => !stackResult.Any(s => s.ContainsFile(i.Path, i.IsDirectory)))
+ .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
.ToList();
var list = new List<VideoInfo>();
@@ -48,7 +59,9 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)).ToList()
+ Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
+ .OfType<VideoFileInfo>()
+ .ToList()
};
info.Year = info.Files[0].Year;
@@ -133,7 +146,7 @@ namespace Emby.Naming.Video
}
// If there's only one video, accept all trailers
- // Be lenient because people use all kinds of mish mash conventions with trailers
+ // Be lenient because people use all kinds of mishmash conventions with trailers.
if (list.Count == 1)
{
var trailers = remainingFiles
@@ -203,15 +216,21 @@ namespace Emby.Naming.Video
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
}
- private bool IsEligibleForMultiVersion(string folderName, string testFilename)
+ private bool IsEligibleForMultiVersion(string folderName, string? testFilename)
{
testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
+ if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
+ {
+ testFilename = cleanName.ToString();
+ }
+
testFilename = testFilename.Substring(folderName.Length).Trim();
return string.IsNullOrEmpty(testFilename)
- || testFilename[0] == '-'
+ || testFilename[0].Equals('-')
+ || testFilename[0].Equals('_')
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index b4aee614b..d7165d8d7 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
@@ -8,10 +5,18 @@ using Emby.Naming.Common;
namespace Emby.Naming.Video
{
+ /// <summary>
+ /// Resolves <see cref="VideoFileInfo"/> from file path.
+ /// </summary>
public class VideoResolver
{
private readonly NamingOptions _options;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoResolver"/> class.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
+ /// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
public VideoResolver(NamingOptions options)
{
_options = options;
@@ -22,7 +27,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoFileInfo.</returns>
- public VideoFileInfo? ResolveDirectory(string path)
+ public VideoFileInfo? ResolveDirectory(string? path)
{
return Resolve(path, true);
}
@@ -32,7 +37,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoFileInfo.</returns>
- public VideoFileInfo? ResolveFile(string path)
+ public VideoFileInfo? ResolveFile(string? path)
{
return Resolve(path, false);
}
@@ -45,11 +50,11 @@ namespace Emby.Naming.Video
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
/// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
- public VideoFileInfo? Resolve(string path, bool isDirectory, bool parseName = true)
+ public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
{
if (string.IsNullOrEmpty(path))
{
- throw new ArgumentNullException(nameof(path));
+ return null;
}
bool isStub = false;
@@ -99,39 +104,58 @@ namespace Emby.Naming.Video
}
}
- return new VideoFileInfo
- {
- Path = path,
- Container = container,
- IsStub = isStub,
- Name = name,
- Year = year,
- StubType = stubType,
- Is3D = format3DResult.Is3D,
- Format3D = format3DResult.Format3D,
- ExtraType = extraResult.ExtraType,
- IsDirectory = isDirectory,
- ExtraRule = extraResult.Rule
- };
+ return new VideoFileInfo(
+ path: path,
+ container: container,
+ isStub: isStub,
+ name: name,
+ year: year,
+ stubType: stubType,
+ is3D: format3DResult.Is3D,
+ format3D: format3DResult.Format3D,
+ extraType: extraResult.ExtraType,
+ isDirectory: isDirectory,
+ extraRule: extraResult.Rule);
}
+ /// <summary>
+ /// Determines if path is video file based on extension.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <returns>True if is video file.</returns>
public bool IsVideoFile(string path)
{
var extension = Path.GetExtension(path) ?? string.Empty;
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
+ /// <summary>
+ /// Determines if path is video file stub based on extension.
+ /// </summary>
+ /// <param name="path">Path to file.</param>
+ /// <returns>True if is video file stub.</returns>
public bool IsStubFile(string path)
{
var extension = Path.GetExtension(path) ?? string.Empty;
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
+ /// <summary>
+ /// Tries to clean name of clutter.
+ /// </summary>
+ /// <param name="name">Raw name.</param>
+ /// <param name="newName">Clean name.</param>
+ /// <returns>True if cleaning of name was successful.</returns>
public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
{
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
}
+ /// <summary>
+ /// Tries to get name and year from raw name.
+ /// </summary>
+ /// <param name="name">Raw name.</param>
+ /// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
public CleanDateTimeResult CleanDateTime(string name)
{
return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 1d430a5e5..16ee918c4 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index 7116d52b1..7433d3c8a 100644
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ b/Emby.Notifications/NotificationEntryPoint.cs
@@ -83,7 +83,7 @@ namespace Emby.Notifications
return Task.CompletedTask;
}
- private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
+ private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
{
var type = NotificationType.ServerRestartRequired.ToString();
@@ -99,7 +99,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false);
}
- private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+ private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{
var entry = e.Argument;
@@ -132,7 +132,7 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("notifications");
}
- private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
+ private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
{
if (!_appHost.HasUpdateAvailable)
{
@@ -151,7 +151,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false);
}
- private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
+ private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@@ -197,7 +197,7 @@ namespace Emby.Notifications
return item.SourceType == SourceType.Library;
}
- private async void LibraryUpdateTimerCallback(object state)
+ private async void LibraryUpdateTimerCallback(object? state)
{
List<BaseItem> items;
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index dbe01257f..62e33e6c4 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,7 +19,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 4c9ab33a7..77819c764 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Linq;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@@ -35,7 +36,7 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception)
{
- configuration = Activator.CreateInstance(type);
+ configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
}
using var stream = new MemoryStream(buffer?.Length ?? 0);
@@ -48,8 +49,9 @@ namespace Emby.Server.Implementations.AppBase
// If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
+ Directory.CreateDirectory(directory);
// Save it after load in case we got new items
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index ad3c19618..5d47d1e40 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -94,7 +94,6 @@ using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
-using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
@@ -520,7 +519,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
ServiceCollection.AddSingleton(_fileSystemManager);
- ServiceCollection.AddSingleton<TvdbClientManager>();
ServiceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(_networkManager);
@@ -1070,7 +1068,6 @@ namespace Emby.Server.Implementations
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
- versions.RemoveAt(x);
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
@@ -1080,6 +1077,8 @@ namespace Emby.Server.Implementations
{
Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
}
+
+ versions.RemoveAt(x);
}
}
@@ -1378,7 +1377,7 @@ namespace Emby.Server.Implementations
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 19045b72b..3d97a6ca8 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -634,7 +634,7 @@ namespace Emby.Server.Implementations.Channels
{
var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
- if (query.ChannelIds.Length > 0)
+ if (query.ChannelIds.Count > 0)
{
// Avoid implicitly captured closure
var ids = query.ChannelIds;
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index fd302d136..12a9e44e7 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Cryptography;
using static MediaBrowser.Common.Cryptography.Constants;
@@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Cryptography
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
}
- using var h = HashAlgorithm.Create(hashMethod);
+ using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
if (salt.Length == 0)
{
return h.ComputeHash(bytes);
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 70a6df977..1af301ceb 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -107,20 +107,6 @@ namespace Emby.Server.Implementations.Data
return null;
}
- public static void Attach(SQLiteDatabaseConnection db, string path, string alias)
- {
- var commandText = string.Format(
- CultureInfo.InvariantCulture,
- "attach @path as {0};",
- alias);
-
- using (var statement = db.PrepareStatement(commandText))
- {
- statement.TryBind("@path", path);
- statement.MoveNext();
- }
- }
-
public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index)
{
return result[index].SQLiteType == SQLiteType.Null;
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 638c7a9b4..7e01bd4b6 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -3611,12 +3611,12 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add($"type in ({inClause})");
}
- if (query.ChannelIds.Length == 1)
+ if (query.ChannelIds.Count == 1)
{
whereClauses.Add("ChannelId=@ChannelId");
statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
}
- else if (query.ChannelIds.Length > 1)
+ else if (query.ChannelIds.Count > 1)
{
var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add($"ChannelId in ({inClause})");
@@ -4076,7 +4076,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause);
}
- if (query.GenreIds.Length > 0)
+ if (query.GenreIds.Count > 0)
{
var clauses = new List<string>();
var index = 0;
@@ -4097,7 +4097,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause);
}
- if (query.Genres.Length > 0)
+ if (query.Genres.Count > 0)
{
var clauses = new List<string>();
var index = 0;
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index f98c694c4..da5047d24 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -1,61 +1,38 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Caching.Memory;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
- private readonly IMemoryCache _memoryCache;
- private readonly IJsonSerializer _json;
private readonly IUserManager _userManager;
- private readonly IServerConfigurationManager _config;
private readonly IAuthenticationRepository _authRepo;
- private readonly object _capabilitiesSyncLock = new object();
+ private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
- public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
-
- public DeviceManager(
- IAuthenticationRepository authRepo,
- IJsonSerializer json,
- IUserManager userManager,
- IServerConfigurationManager config,
- IMemoryCache memoryCache)
+ public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
{
- _json = json;
_userManager = userManager;
- _config = config;
- _memoryCache = memoryCache;
_authRepo = authRepo;
}
+ public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
+
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{
- var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json");
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- lock (_capabilitiesSyncLock)
- {
- _memoryCache.Set(deviceId, capabilities);
- _json.SerializeToFile(capabilities, path);
- }
+ _capabilitiesMap[deviceId] = capabilities;
}
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
@@ -72,33 +49,13 @@ namespace Emby.Server.Implementations.Devices
public ClientCapabilities GetCapabilities(string id)
{
- if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
- {
- return result;
- }
-
- lock (_capabilitiesSyncLock)
- {
- var path = Path.Combine(GetDevicePath(id), "capabilities.json");
- try
- {
- return _json.DeserializeFromFile<ClientCapabilities>(path) ?? new ClientCapabilities();
- }
- catch
- {
- }
- }
-
- return new ClientCapabilities();
+ return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
+ ? result
+ : new ClientCapabilities();
}
public DeviceInfo GetDevice(string id)
{
- return GetDevice(id, true);
- }
-
- private DeviceInfo GetDevice(string id, bool includeCapabilities)
- {
var session = _authRepo.Get(new AuthenticationInfoQuery
{
DeviceId = id
@@ -154,16 +111,6 @@ namespace Emby.Server.Implementations.Devices
};
}
- private string GetDevicesPath()
- {
- return Path.Combine(_config.ApplicationPaths.DataPath, "devices");
- }
-
- private string GetDevicePath(string id)
- {
- return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
- }
-
public bool CanAccessDevice(User user, string deviceId)
{
if (user == null)
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index c762aa0b8..d360bb00f 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -32,13 +32,13 @@
<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.9" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
- <PackageReference Include="Mono.Nat" Version="3.0.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
- <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
+ <PackageReference Include="ServiceStack.Text.Core" Version="5.10.0" />
<PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.0" />
@@ -49,10 +49,12 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
+ <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
+ <NoWarn>AD0001</NoWarn>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 7074382b6..8ffb05e1c 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio;
@@ -1502,7 +1503,7 @@ namespace Emby.Server.Implementations.Library
{
if (query.AncestorIds.Length == 0 &&
query.ParentId.Equals(Guid.Empty) &&
- query.ChannelIds.Length == 0 &&
+ query.ChannelIds.Count == 0 &&
query.TopParentIds.Length == 0 &&
string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
@@ -2485,9 +2486,10 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
+ // TODO nullable - what are we trying to do there with empty episodeInfo?
var episodeInfo = episode.IsFileProtocol
- ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
- : new Naming.TV.EpisodeInfo();
+ ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path)
+ : new Naming.TV.EpisodeInfo(episode.Path);
try
{
@@ -2576,12 +2578,12 @@ namespace Emby.Server.Implementations.Library
if (!episode.IndexNumberEnd.HasValue || forceRefresh)
{
- if (episode.IndexNumberEnd != episodeInfo.EndingEpsiodeNumber)
+ if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
{
changed = true;
}
- episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber;
+ episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
}
if (!episode.ParentIndexNumber.HasValue || forceRefresh)
@@ -2907,7 +2909,7 @@ namespace Emby.Server.Implementations.Library
return item.GetImageInfo(image.Type, imageIndex);
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
if (ex.StatusCode.HasValue
&& (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 179e0ed98..28fa06239 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.Library
private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences)
{
- // Give some preferance to external text subs for better performance
+ // Give some preference to external text subs for better performance
return streams.Where(i => i.Type == type)
.OrderBy(i =>
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 44560d1e2..341194f23 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -77,11 +77,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
// The media source if infinite so we need to handle stopping ourselves
- var durationToken = new CancellationTokenSource(duration);
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ using var durationToken = new CancellationTokenSource(duration);
+ using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
+ cancellationToken = linkedCancellationToken.Token;
await _streamHelper.CopyUntilCancelled(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
output,
IODefaults.CopyToBufferSize,
cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index fcc2d1eeb..0dc045ee6 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1635,7 +1635,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{
- return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
+ return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
}
return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 3e5457dbd..e6ee9819e 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -8,7 +8,9 @@ using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
@@ -25,6 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IServerApplicationPaths _appPaths;
private readonly IJsonSerializer _json;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
+ private readonly IServerConfigurationManager _serverConfigurationManager;
private bool _hasExited;
private Stream _logFileStream;
@@ -35,12 +38,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
ILogger logger,
IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths,
- IJsonSerializer json)
+ IJsonSerializer json,
+ IServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
_json = json;
+ _serverConfigurationManager = serverConfigurationManager;
}
private static bool CopySubtitles => false;
@@ -179,15 +184,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var outputParam = string.Empty;
+ var threads = EncodingHelper.GetNumberOfThreads(null, _serverConfigurationManager.GetEncodingOptions(), null);
var commandLineArgs = string.Format(
CultureInfo.InvariantCulture,
- "-i \"{0}\" {2} -map_metadata -1 -threads 0 {3}{4}{5} -y \"{1}\"",
+ "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile,
targetFile,
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
- outputParam);
+ outputParam,
+ threads);
return inputModifier + " " + commandLineArgs;
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index aacadde50..5d17ba1de 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json);
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
- await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
@@ -123,7 +123,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
- await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
@@ -261,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Id = newID,
StartDate = startAt,
EndDate = endAt,
- Name = details.titles[0].title120 ?? "Unkown",
+ Name = details.titles[0].title120 ?? "Unknown",
OfficialRating = null,
CommunityRating = null,
EpisodeTitle = episodeTitle,
@@ -480,9 +480,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
- await using var response = await innerResponse2.Content.ReadAsStreamAsync().ConfigureAwait(false);
- return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
- response).ConfigureAwait(false);
+ await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(response).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -509,7 +508,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
- await using var response = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
@@ -542,6 +541,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private DateTime _lastErrorResponse;
+
private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken)
{
var username = info.Username;
@@ -591,7 +591,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
return result;
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
if (ex.StatusCode.HasValue)
{
@@ -621,7 +621,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
_tokens.Clear();
@@ -651,7 +651,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
if (root.message == "OK")
{
@@ -705,13 +705,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
- await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
// Apparently we're supposed to swallow this
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
@@ -780,7 +780,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var list = new List<ChannelInfo>();
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
- await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel");
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 2d6f453bd..76c875737 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -79,7 +79,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
{
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 2f4c60117..c0a4d1228 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
@@ -129,7 +129,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -143,7 +143,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return discoverResponse;
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
{
@@ -175,7 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List<LiveTvTunerInfo>();
while (!sr.EndOfStream)
@@ -663,7 +663,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
info.DeviceId = modelInfo.DeviceID;
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 7c13d45e9..1d6c26c13 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
.SendAsync(requestMessage, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
}
return File.OpenRead(info.Url);
@@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (string.IsNullOrWhiteSpace(numberString))
{
// Using this as a fallback now as this leads to Problems with channels like "5 USA"
- // where 5 isnt ment to be the channel number
+ // where 5 isn't ment to be the channel number
// Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 2e1b89509..2de447ad9 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
using var message = response;
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
await StreamHelper.CopyToAsync(
stream,
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index fb31b01ff..775267183 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -115,5 +115,8 @@
"TasksLibraryCategory": "Knihovna",
"TasksMaintenanceCategory": "Údržba",
"TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.",
- "TaskCleanActivityLog": "Smazat záznam aktivity"
+ "TaskCleanActivityLog": "Smazat záznam aktivity",
+ "Undefined": "Nedefinované",
+ "Forced": "Vynucené",
+ "Default": "Výchozí"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index c81de8218..6ab22b8a4 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -115,5 +115,8 @@
"TasksLibraryCategory": "Bibliothek",
"TasksMaintenanceCategory": "Wartung",
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
- "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen"
+ "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen",
+ "Undefined": "Undefiniert",
+ "Forced": "Erzwungen",
+ "Default": "Standard"
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index c45cc11cb..dcf3311cd 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -113,5 +113,10 @@
"TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
"TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
"TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
- "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας."
+ "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.",
+ "TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής δραστηριοτήτων παλαιότερες από την ηλικία που έχει διαμορφωθεί.",
+ "TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
+ "Undefined": "Απροσδιόριστο",
+ "Forced": "Εξαναγκασμένο",
+ "Default": "Προεπιλογή"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 6d8b222b4..f8f595faa 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -9,11 +9,13 @@
"Channels": "Channels",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
+ "Default": "Default",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
+ "Forced": "Forced",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
"HeaderContinueWatching": "Continue Watching",
@@ -77,6 +79,7 @@
"Sync": "Sync",
"System": "System",
"TvShows": "TV Shows",
+ "Undefined": "Undefined",
"User": "User",
"UserCreatedWithName": "User {0} has been created",
"UserDeletedWithName": "User {0} has been deleted",
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 390074cdd..0d4a14be0 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -113,5 +113,10 @@
"TasksChannelsCategory": "Canales de internet",
"TasksApplicationCategory": "Aplicación",
"TasksLibraryCategory": "Biblioteca",
- "TasksMaintenanceCategory": "Mantenimiento"
+ "TasksMaintenanceCategory": "Mantenimiento",
+ "TaskCleanActivityLogDescription": "Borrar log de actividades anteriores a la fecha establecida.",
+ "TaskCleanActivityLog": "Borrar log de actividades",
+ "Undefined": "Indefinido",
+ "Forced": "Forzado",
+ "Default": "Por Defecto"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index d6af40c40..fe674cf36 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -115,5 +115,8 @@
"TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan",
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos.",
"TaskCleanActivityLogDescription": "Elimina todos los registros de actividad anteriores a la fecha configurada.",
- "TaskCleanActivityLog": "Limpiar registro de actividad"
+ "TaskCleanActivityLog": "Limpiar registro de actividad",
+ "Undefined": "Indefinido",
+ "Forced": "Forzado",
+ "Default": "Predeterminado"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index cc9243f37..3d5d69f36 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -115,5 +115,8 @@
"TasksLibraryCategory": "Bibliothèque",
"TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
- "TaskCleanActivityLog": "Nettoyer le journal d'activité"
+ "TaskCleanActivityLog": "Nettoyer le journal d'activité",
+ "Undefined": "Non défini",
+ "Forced": "Forcé",
+ "Default": "Par défaut"
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index f906d6e11..981e8a06e 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -113,5 +113,10 @@
"TaskRefreshChannels": "רענן ערוץ",
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
"TaskCleanTranscode": "נקה תקיית Transcode",
- "TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי."
+ "TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי.",
+ "TaskCleanActivityLogDescription": "מחק רשומת פעילות הישנה יותר מהגיל המוגדר.",
+ "TaskCleanActivityLog": "נקה רשומת פעילות",
+ "Undefined": "לא מוגדר",
+ "Forced": "כפוי",
+ "Default": "ברירת מחדל"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 804dabe57..e5707e78c 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -115,5 +115,8 @@
"TaskRefreshChannels": "Csatornák frissítése",
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
"TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
- "TaskCleanActivityLog": "Tevékenységnapló törlése"
+ "TaskCleanActivityLog": "Tevékenységnapló törlése",
+ "Undefined": "Meghatározatlan",
+ "Forced": "Kényszerített",
+ "Default": "Alapértelmezett"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 245c3cd63..3b016fe62 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -113,5 +113,9 @@
"TaskRefreshPeople": "Oppfrisk personer",
"TaskCleanLogsDescription": "Sletter loggfiler som er eldre enn {0} dager gamle.",
"TaskCleanLogs": "Tøm loggmappe",
- "TaskRefreshLibraryDescription": "Skanner mediebibliotekene dine for nye filer og oppdaterer metadata."
+ "TaskRefreshLibraryDescription": "Skanner mediebibliotekene dine for nye filer og oppdaterer metadata.",
+ "TaskCleanActivityLog": "Tøm aktivitetslogg",
+ "Undefined": "Udefinert",
+ "Forced": "Tvungen",
+ "Default": "Standard"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index 003e591b3..e3da96a85 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -113,5 +113,10 @@
"TasksChannelsCategory": "Kanały internetowe",
"TasksApplicationCategory": "Aplikacja",
"TasksLibraryCategory": "Biblioteka",
- "TasksMaintenanceCategory": "Konserwacja"
+ "TasksMaintenanceCategory": "Konserwacja",
+ "TaskCleanActivityLogDescription": "Usuwa wpisy dziennika aktywności starsze niż skonfigurowany wiek.",
+ "TaskCleanActivityLog": "Czyść dziennik aktywności",
+ "Undefined": "Nieustalony",
+ "Forced": "Wymuszony",
+ "Default": "Domyślne"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 5e49ca702..8d25e27f6 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -113,5 +113,7 @@
"TasksChannelsCategory": "Canais da Internet",
"TasksApplicationCategory": "Aplicativo",
"TasksLibraryCategory": "Biblioteca",
- "TasksMaintenanceCategory": "Manutenção"
+ "TasksMaintenanceCategory": "Manutenção",
+ "TaskCleanActivityLogDescription": "Apaga o registro de atividades mais antigo que a idade configurada.",
+ "TaskCleanActivityLog": "Limpar Registro de Atividades"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index e8cd23d5d..5fcdb1f74 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -114,5 +114,8 @@
"UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
"UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது",
"TaskCleanActivityLogDescription": "உள்ளமைக்கப்பட்ட வயதை விட பழைய செயல்பாட்டு பதிவு உள்ளீடுகளை நீக்குகிறது.",
- "TaskCleanActivityLog": "செயல்பாட்டு பதிவை அழி"
+ "TaskCleanActivityLog": "செயல்பாட்டு பதிவை அழி",
+ "Undefined": "வரையறுக்கப்படாத",
+ "Forced": "கட்டாயப்படுத்தப்பட்டது",
+ "Default": "இயல்புநிலை"
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index ba58e4beb..0549995c8 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -114,5 +114,8 @@
"Application": "Ứng Dụng",
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}",
"TaskCleanActivityLogDescription": "Xóa các mục nhật ký hoạt động cũ hơn độ tuổi đã cài đặt.",
- "TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động"
+ "TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động",
+ "Undefined": "Không Xác Định",
+ "Forced": "Bắt Buộc",
+ "Default": "Mặc Định"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 3ae0fe5e7..12803456e 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -115,5 +115,8 @@
"TasksApplicationCategory": "应用程序",
"TasksMaintenanceCategory": "维护",
"TaskCleanActivityLog": "清理程序日志",
- "TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。"
+ "TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。",
+ "Undefined": "未定义",
+ "Forced": "强制的",
+ "Default": "默认"
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index d3b64fb31..932f721ab 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.Playlists
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
.ConfigureAwait(false);
- if (options.ItemIdList.Length > 0)
+ if (options.ItemIdList.Count > 0)
{
await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
{
@@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
}
- public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId)
+ public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{
var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
@@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Playlists
});
}
- private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options)
+ private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options)
{
// Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 6f81bf49b..cfbf03ddc 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -136,7 +136,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
var type = scheduledTask.ScheduledTask.GetType();
- _logger.LogInformation("Queueing task {0}", type.Name);
+ _logger.LogInformation("Queuing task {0}", type.Name);
lock (_taskQueue)
{
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
var type = task.ScheduledTask.GetType();
- _logger.LogInformation("Queueing task {0}", type.Name);
+ _logger.LogInformation("Queuing task {0}", type.Name);
lock (_taskQueue)
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index c5af68bce..161fa0580 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
@@ -101,7 +102,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw;
}
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
_logger.LogError(ex, "Error downloading {0}", package.Name);
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 607b322f2..afddfa856 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -58,8 +58,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// The active connections.
/// </summary>
- private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
- new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new (StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
@@ -196,7 +195,7 @@ namespace Emby.Server.Implementations.Session
{
if (!string.IsNullOrEmpty(info.DeviceId))
{
- var capabilities = GetSavedCapabilities(info.DeviceId);
+ var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
if (capabilities != null)
{
@@ -1677,27 +1676,10 @@ namespace Emby.Server.Implementations.Session
SessionInfo = session
});
- try
- {
- SaveCapabilities(session.DeviceId, capabilities);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error saving device capabilities", ex);
- }
+ _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
}
}
- private ClientCapabilities GetSavedCapabilities(string deviceId)
- {
- return _deviceManager.GetCapabilities(deviceId);
- }
-
- private void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
- {
- _deviceManager.SaveCapabilities(deviceId, capabilities);
- }
-
/// <summary>
/// Converts a BaseItem to a BaseItemInfo.
/// </summary>
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index b986ffa1c..f9c6a13c6 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
@@ -55,9 +56,9 @@ namespace Emby.Server.Implementations.Session
connection.Closed += OnConnectionClosed;
}
- private void OnConnectionClosed(object sender, EventArgs e)
+ private void OnConnectionClosed(object? sender, EventArgs e)
{
- var connection = (IWebSocketConnection)sender;
+ var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
_sockets.Remove(connection);
connection.Closed -= OnConnectionClosed;
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 966ed5024..7c4e00311 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.SyncPlay
new Dictionary<Guid, ISyncPlayController>();
/// <summary>
- /// Lock used for accesing any group.
+ /// Lock used for accessing any group.
/// </summary>
private readonly object _groupsLock = new object();
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index fd1f43e62..851e7bd68 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Updates
{
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(manifest, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
try
{
@@ -116,11 +116,6 @@ namespace Emby.Server.Implementations.Updates
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
- catch (HttpException ex)
- {
- _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
- return Array.Empty<PackageInfo>();
- }
catch (HttpRequestException ex)
{
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
@@ -246,7 +241,8 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Add(tuple);
}
- var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
+ using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token);
+ var linkedToken = linkedTokenSource.Token;
await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false);
@@ -338,7 +334,7 @@ namespace Emby.Server.Implementations.Updates
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(package.SourceUrl, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 9bad206e0..f684c649a 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -89,24 +89,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -131,30 +131,26 @@ namespace Jellyfin.Api.Controllers
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
- var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
- var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
- var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
-
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = excludeItemTypesArr,
- IncludeItemTypes = includeItemTypesArr,
- MediaTypes = mediaTypesArr,
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
@@ -174,9 +170,9 @@ namespace Jellyfin.Api.Controllers
}
// Studios
- if (!string.IsNullOrEmpty(studios))
+ if (studios.Length != 0)
{
- query.StudioIds = studios.Split('|').Select(i =>
+ query.StudioIds = studios.Select(i =>
{
try
{
@@ -230,7 +226,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
+ if (includeItemTypes.Length != 0)
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
@@ -297,24 +293,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -339,30 +335,26 @@ namespace Jellyfin.Api.Controllers
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
- var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
- var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
- var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
-
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = excludeItemTypesArr,
- IncludeItemTypes = includeItemTypesArr,
- MediaTypes = mediaTypesArr,
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
@@ -382,9 +374,9 @@ namespace Jellyfin.Api.Controllers
}
// Studios
- if (!string.IsNullOrEmpty(studios))
+ if (studios.Length != 0)
{
- query.StudioIds = studios.Split('|').Select(i =>
+ query.StudioIds = studios.Select(i =>
{
try
{
@@ -438,7 +430,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
+ if (includeItemTypes.Length != 0)
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 330e56614..c22979495 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
@@ -42,7 +42,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index ec9d7cdce..c4dc44cc3 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -198,7 +198,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? channelIds)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -208,11 +208,7 @@ namespace Jellyfin.Api.Controllers
{
Limit = limit,
StartIndex = startIndex,
- ChannelIds = (channelIds ?? string.Empty)
- .Split(',')
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Select(i => new Guid(i))
- .ToArray(),
+ ChannelIds = channelIds,
DtoOptions = new DtoOptions { Fields = fields }
};
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index eae06b767..2a342c2cb 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Net;
@@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name,
- [FromQuery] string? ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
[FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false)
{
@@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers
IsLocked = isLocked,
Name = name,
ParentId = parentId,
- ItemIdList = RequestHelpers.Split(ids, ',', true),
+ ItemIdList = ids,
UserIds = new[] { userId }
}).ConfigureAwait(false);
@@ -88,9 +89,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
+ public async Task<ActionResult> AddToCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
- await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true);
+ await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
return NoContent();
}
@@ -103,9 +106,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
+ public async Task<ActionResult> RemoveFromCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
- await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false);
+ await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index a6b249ccf..eff5bd54a 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -14,6 +15,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
@@ -40,6 +42,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
public class DynamicHlsController : BaseJellyfinApiController
{
+ private const string DefaultEncoderPreset = "veryfast";
+ private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
@@ -55,8 +60,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper;
-
- private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
+ private readonly EncodingOptions _encodingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
@@ -91,6 +95,8 @@ namespace Jellyfin.Api.Controllers
ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper)
{
+ _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+
_libraryManager = libraryManager;
_userManager = userManager;
_dlnaManager = dlnaManager;
@@ -105,8 +111,7 @@ namespace Jellyfin.Api.Controllers
_transcodingJobHelper = transcodingJobHelper;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
-
- _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+ _encodingOptions = serverConfigurationManager.GetEncodingOptions();
}
/// <summary>
@@ -119,7 +124,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -148,7 +153,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -271,7 +276,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
@@ -284,7 +289,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -314,7 +319,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -438,7 +443,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
@@ -451,7 +456,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -480,7 +485,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -614,7 +619,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -644,7 +649,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -811,7 +816,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -952,7 +957,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -982,7 +987,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -1128,7 +1133,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
- _transcodingJobType,
+ TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@@ -1136,11 +1141,19 @@ namespace Jellyfin.Api.Controllers
var segmentLengths = GetSegmentLengths(state);
+ var segmentContainer = state.Request.SegmentContainer ?? "ts";
+
+ // http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
+ var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
+ var hlsVersion = isHlsInFmp4 ? "7" : "3";
+
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
- .AppendLine("#EXT-X-VERSION:3")
+ .Append("#EXT-X-VERSION:")
+ .Append(hlsVersion)
+ .AppendLine()
.Append("#EXT-X-TARGETDURATION:")
.Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
.AppendLine()
@@ -1150,6 +1163,18 @@ namespace Jellyfin.Api.Controllers
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
var queryString = Request.QueryString;
+ if (isHlsInFmp4)
+ {
+ builder.Append("#EXT-X-MAP:URI=\"")
+ .Append("hls1/")
+ .Append(name)
+ .Append("/-1")
+ .Append(segmentExtension)
+ .Append(queryString)
+ .Append('"')
+ .AppendLine();
+ }
+
foreach (var length in segmentLengths)
{
builder.Append("#EXTINF:")
@@ -1193,7 +1218,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
- _transcodingJobType,
+ TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@@ -1207,7 +1232,7 @@ namespace Jellyfin.Api.Controllers
if (System.IO.File.Exists(segmentPath))
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
_logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@@ -1221,7 +1246,7 @@ namespace Jellyfin.Api.Controllers
{
if (System.IO.File.Exists(segmentPath))
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release();
released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
@@ -1232,7 +1257,13 @@ namespace Jellyfin.Api.Controllers
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
- if (currentTranscodingIndex == null)
+ if (segmentId == -1)
+ {
+ _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
+ startTranscoding = true;
+ segmentId = 0;
+ }
+ else if (currentTranscodingIndex == null)
{
_logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
startTranscoding = true;
@@ -1264,13 +1295,12 @@ namespace Jellyfin.Api.Controllers
streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
state.WaitForPath = segmentPath;
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
job = await _transcodingJobHelper.StartFfMpeg(
state,
playlistPath,
- GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId),
+ GetCommandLineArguments(playlistPath, state, true, segmentId),
Request,
- _transcodingJobType,
+ TranscodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
catch
@@ -1283,7 +1313,7 @@ namespace Jellyfin.Api.Controllers
}
else
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler != null)
{
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
@@ -1300,7 +1330,7 @@ namespace Jellyfin.Api.Controllers
}
_logger.LogDebug("returning {0} [general case]", segmentPath);
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@@ -1324,11 +1354,10 @@ namespace Jellyfin.Api.Controllers
return result.ToArray();
}
- private string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding, int startNumber)
+ private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber)
{
- var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var threads = _encodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
if (state.BaseRequest.BreakOnNonKeyFrames)
{
@@ -1340,34 +1369,57 @@ namespace Jellyfin.Api.Controllers
state.BaseRequest.BreakOnNonKeyFrames = false;
}
- var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
-
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
-
+ var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
- var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer);
+ var outputTsArg = outputPrefix + "%d" + outputExtension;
- var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
+ var segmentFormat = outputExtension.TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
+ else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var outputFmp4HeaderArg = string.Empty;
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ if (isWindows)
+ {
+ // on Windows, the path of fmp4 header file needs to be configured
+ outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
+ }
+ else
+ {
+ // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
+ outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
+ }
- var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
- ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+ segmentFormat = "fmp4" + outputFmp4HeaderArg;
+ }
+ else
+ {
+ _logger.LogError("Invalid HLS segment container: " + segmentFormat);
+ }
+
+ var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
+ ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
: "128";
return string.Format(
CultureInfo.InvariantCulture,
- "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
+ "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
inputModifier,
- _encodingHelper.GetInputArgument(state, encodingOptions),
+ _encodingHelper.GetInputArgument(state, _encodingOptions),
threads,
mapArgs,
- GetVideoArguments(state, encodingOptions, startNumber),
- GetAudioArguments(state, encodingOptions),
+ GetVideoArguments(state, startNumber),
+ GetAudioArguments(state),
maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
segmentFormat,
@@ -1376,50 +1428,63 @@ namespace Jellyfin.Api.Controllers
outputPath).Trim();
}
- private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
+ /// <summary>
+ /// Gets the audio arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <returns>The command line arguments for audio transcoding.</returns>
+ private string GetAudioArguments(StreamState state)
{
+ if (state.AudioStream == null)
+ {
+ return string.Empty;
+ }
+
var audioCodec = _encodingHelper.GetAudioEncoder(state);
if (!state.IsOutputVideo)
{
if (EncodingHelper.IsCopyCodec(audioCodec))
{
- return "-acodec copy";
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+ return "-acodec copy -strict -2" + bitStreamArgs;
}
- var audioTranscodeParams = new List<string>();
+ var audioTranscodeParams = string.Empty;
- audioTranscodeParams.Add("-acodec " + audioCodec);
+ audioTranscodeParams += "-acodec " + audioCodec;
if (state.OutputAudioBitrate.HasValue)
{
- audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
+ audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioChannels.HasValue)
{
- audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
+ audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
- audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
+ audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- audioTranscodeParams.Add("-vn");
- return string.Join(' ', audioTranscodeParams);
+ audioTranscodeParams += " -vn";
+ return audioTranscodeParams;
}
if (EncodingHelper.IsCopyCodec(audioCodec))
{
- var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
{
- return "-codec:a:0 copy -copypriorss:a:0 0";
+ return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs;
}
- return "-codec:a:0 copy";
+ return "-codec:a:0 copy -strict -2" + bitStreamArgs;
}
var args = "-codec:a:0 " + audioCodec;
@@ -1443,94 +1508,89 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true);
+ args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args;
}
- private string GetVideoArguments(StreamState state, EncodingOptions encodingOptions, int startNumber)
+ /// <summary>
+ /// Gets the video arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <param name="startNumber">The first number in the hls sequence.</param>
+ /// <returns>The command line arguments for video transcoding.</returns>
+ private string GetVideoArguments(StreamState state, int startNumber)
{
+ if (state.VideoStream == null)
+ {
+ return string.Empty;
+ }
+
if (!state.IsOutputVideo)
{
return string.Empty;
}
- var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+ var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var args = "-codec:v:0 " + codec;
+ // Prefer hvc1 to hev1.
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ args += " -tag:v:0 hvc1";
+ }
+
// if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
- // See if we can save come cpu cycles by avoiding encoding
+ // See if we can save come cpu cycles by avoiding encoding.
if (EncodingHelper.IsCopyCodec(codec))
{
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+ string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;
}
}
+ args += " -start_at_zero";
+
// args += " -flags -global_header";
}
else
{
- var gopArg = string.Empty;
- var keyFrameArg = string.Format(
- CultureInfo.InvariantCulture,
- " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
- startNumber * state.SegmentLength,
- state.SegmentLength);
-
- var framerate = state.VideoStream?.RealFrameRate;
-
- if (framerate.HasValue)
- {
- // This is to make sure keyframe interval is limited to our segment,
- // as forcing keyframes is not enough.
- // Example: we encoded half of desired length, then codec detected
- // scene cut and inserted a keyframe; next forced keyframe would
- // be created outside of segment, which breaks seeking
- // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
- gopArg = string.Format(
- CultureInfo.InvariantCulture,
- " -g {0} -keyint_min {0} -sc_threshold 0",
- Math.Ceiling(state.SegmentLength * framerate.Value));
- }
+ args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
- args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast");
+ // Set the key frame params for video encoding to match the hls segment time.
+ args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber);
- // Unable to force key frames using these hw encoders, set key frames by GOP
- if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
- {
- args += " " + gopArg;
- }
- else
+ // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+ if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
- args += " " + keyFrameArg + gopArg;
+ args += " -bf 0";
}
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
- // This is for graphical subs
if (hasGraphicalSubs)
{
- args += _encodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
+ // Graphical subs overlay and resolution params.
+ args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
}
-
- // Add resolution params, if specified
else
{
- args += _encodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
+ // Resolution params.
+ args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
}
// -start_at_zero is necessary to use with -ss when seeking,
@@ -1565,8 +1625,7 @@ namespace Jellyfin.Api.Controllers
private string GetSegmentPath(StreamState state, string playlist, int index)
{
- var folder = Path.GetDirectoryName(playlist);
-
+ var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
var filename = Path.GetFileNameWithoutExtension(playlist);
return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer));
@@ -1691,7 +1750,7 @@ namespace Jellyfin.Api.Controllers
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{
- var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType);
+ var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
if (job == null || job.HasExited)
{
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index ce88b0b99..b0b4b5af5 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.EnvironmentDtos;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -16,7 +17,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Environment Controller.
/// </summary>
- [Authorize(Policy = Policies.RequiresElevation)]
+ [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
public class EnvironmentController : BaseJellyfinApiController
{
private const char UncSeparator = '\\';
@@ -103,6 +104,11 @@ namespace Jellyfin.Api.Controllers
if (validatePathDto.ValidateWritable)
{
+ if (validatePathDto.Path == null)
+ {
+ throw new ResourceNotFoundException(nameof(validatePathDto.Path));
+ }
+
var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
try
{
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 008bb58d1..c97a1ed14 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -50,8 +51,8 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
[FromQuery] Guid? userId,
[FromQuery] string? parentId,
- [FromQuery] string? includeItemTypes,
- [FromQuery] string? mediaTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
{
var parentItem = string.IsNullOrEmpty(parentId)
? null
@@ -61,10 +62,11 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value)
: null;
- if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+ if (includeItemTypes.Length == 1
+ && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{
parentItem = null;
}
@@ -78,8 +80,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery
{
User = user,
- MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
- IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
+ MediaTypes = mediaTypes,
+ IncludeItemTypes = includeItemTypes,
Recursive = true,
EnableTotalRecordCount = false,
DtoOptions = new DtoOptions
@@ -139,7 +141,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId,
[FromQuery] string? parentId,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isAiring,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSports,
@@ -156,10 +158,11 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value)
: null;
- if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+ if (includeItemTypes.Length == 1
+ && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{
parentItem = null;
}
@@ -167,8 +170,7 @@ namespace Jellyfin.Api.Controllers
var filters = new QueryFilters();
var genreQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes =
- (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
+ IncludeItemTypes = includeItemTypes,
DtoOptions = new DtoOptions
{
Fields = Array.Empty<ItemFields>(),
@@ -192,10 +194,11 @@ namespace Jellyfin.Api.Controllers
genreQuery.Parent = parentItem;
}
- if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
+ if (includeItemTypes.Length == 1
+ && (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase)))
{
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
{
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 89f4beefd..2dd504770 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
result = _libraryManager.GetGenres(query);
}
- var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
@@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
- private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+ private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
where T : BaseItem, new()
{
var result = libraryManager.GetItemList(new InternalItemsQuery
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 054e586ce..ccdbbb297 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
@@ -111,11 +112,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="segmentId">The segment id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <response code="200">Hls video segment returned.</response>
+ /// <response code="404">Hls segment not found.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
// [Authenticated]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsVideoSegmentLegacy(
@@ -131,12 +134,25 @@ namespace Jellyfin.Api.Controllers
var normalizedPlaylistId = playlistId;
- var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
- .FirstOrDefault(i =>
- string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
- && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
+ var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
+ // Add . to start of segment container for future use.
+ segmentContainer = segmentContainer.Insert(0, ".");
+ string? playlistPath = null;
+ foreach (var path in filePaths)
+ {
+ var pathExtension = Path.GetExtension(path);
+ if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
+ && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ playlistPath = path;
+ break;
+ }
+ }
- return GetFileResult(file, playlistPath);
+ return playlistPath == null
+ ? NotFound("Hls segment not found.")
+ : GetFileResult(file, playlistPath);
}
private ActionResult GetFileResult(string path, string playlistPath)
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 980c3273d..198dbc51f 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -161,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 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))
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index bb0f27312..1a8037614 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -1898,7 +1899,7 @@ namespace Jellyfin.Api.Controllers
Response.Headers.Add(key, value);
}
- Response.ContentType = imageContentType;
+ Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index ab73aa428..a7c1a6388 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -334,10 +334,16 @@ namespace Jellyfin.Api.Controllers
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
{
using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
+ if (result.Content.Headers.ContentType?.MediaType == null)
+ {
+ throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
+ }
+
var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
- Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+ var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
+ Directory.CreateDirectory(directory);
using (var stream = result.Content)
{
await using var fileStream = new FileStream(
@@ -351,7 +357,9 @@ namespace Jellyfin.Api.Controllers
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
- Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+ var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
+
+ Directory.CreateDirectory(pointerCacheDirectory);
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 63985c72a..7194bf2a9 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
@@ -72,8 +72,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
/// <param name="isHd">Optional filter by items that are HD or not.</param>
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
@@ -86,42 +86,42 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
- /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
- /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
- /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
- /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
- /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="isLocked">Optional filter by items that are locked.</param>
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
@@ -132,12 +132,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
- /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
@@ -156,7 +156,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
- [FromQuery] string? locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
@@ -170,7 +170,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId,
- [FromQuery] string? excludeItemIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
@@ -178,34 +178,34 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
- [FromQuery] string? genres,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? artists,
- [FromQuery] string? excludeArtistIds,
- [FromQuery] string? artistIds,
- [FromQuery] string? albumArtistIds,
- [FromQuery] string? contributingArtistIds,
- [FromQuery] string? albums,
- [FromQuery] string? albumIds,
- [FromQuery] string? ids,
- [FromQuery] string? videoTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@@ -216,12 +216,12 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
- [FromQuery] string? seriesStatus,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery] string? studioIds,
- [FromQuery] string? genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@@ -232,8 +232,9 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
+ if (includeItemTypes.Length == 1
+ && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase)
+ || includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase)))
{
parentId = null;
}
@@ -256,7 +257,7 @@ namespace Jellyfin.Api.Controllers
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
recursive = true;
- includeItemTypes = "Playlist";
+ includeItemTypes = new[] { "Playlist" };
}
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
@@ -285,14 +286,14 @@ namespace Jellyfin.Api.Controllers
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
}
- if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder))
+ if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
{
var query = new InternalItemsQuery(user!)
{
IsPlayed = isPlayed,
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ MediaTypes = mediaTypes,
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
Recursive = recursive ?? false,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
IsFavorite = isFavorite,
@@ -324,28 +325,28 @@ namespace Jellyfin.Api.Controllers
HasTrailer = hasTrailer,
IsHD = isHd,
Is4K = is4K,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- ArtistIds = RequestHelpers.GetGuids(artistIds),
- AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds),
- ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ ArtistIds = artistIds,
+ AlbumArtistIds = albumArtistIds,
+ ContributingArtistIds = contributingArtistIds,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
ImageTypes = imageTypes,
- VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
+ VideoTypes = videoTypes,
AdjacentTo = adjacentTo,
- ItemIds = RequestHelpers.GetGuids(ids),
+ ItemIds = ids,
MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating,
ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount,
- ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds),
+ ExcludeItemIds = excludeItemIds,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
@@ -354,7 +355,7 @@ namespace Jellyfin.Api.Controllers
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
};
- if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm))
+ if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
{
query.CollapseBoxSetItems = false;
}
@@ -394,9 +395,9 @@ namespace Jellyfin.Api.Controllers
}
// Filter by Series Status
- if (!string.IsNullOrEmpty(seriesStatus))
+ if (seriesStatus.Length != 0)
{
- query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
+ query.SeriesStatuses = seriesStatus;
}
// ExcludeLocationTypes
@@ -405,13 +406,9 @@ namespace Jellyfin.Api.Controllers
query.IsVirtualItem = false;
}
- if (!string.IsNullOrEmpty(locationTypes))
+ if (locationTypes.Length > 0 && locationTypes.Length < 4)
{
- var requestedLocationTypes = locationTypes.Split(',');
- if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
- {
- query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
- }
+ query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
}
// Min official rating
@@ -427,9 +424,9 @@ namespace Jellyfin.Api.Controllers
}
// Artists
- if (!string.IsNullOrEmpty(artists))
+ if (artists.Length != 0)
{
- query.ArtistIds = artists.Split('|').Select(i =>
+ query.ArtistIds = artists.Select(i =>
{
try
{
@@ -443,29 +440,29 @@ namespace Jellyfin.Api.Controllers
}
// ExcludeArtistIds
- if (!string.IsNullOrWhiteSpace(excludeArtistIds))
+ if (excludeArtistIds.Length != 0)
{
- query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+ query.ExcludeArtistIds = excludeArtistIds;
}
- if (!string.IsNullOrWhiteSpace(albumIds))
+ if (albumIds.Length != 0)
{
- query.AlbumIds = RequestHelpers.GetGuids(albumIds);
+ query.AlbumIds = albumIds;
}
// Albums
- if (!string.IsNullOrEmpty(albums))
+ if (albums.Length != 0)
{
- query.AlbumIds = albums.Split('|').SelectMany(i =>
+ query.AlbumIds = albums.SelectMany(i =>
{
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
}).ToArray();
}
// Studios
- if (!string.IsNullOrEmpty(studios))
+ if (studios.Length != 0)
{
- query.StudioIds = studios.Split('|').Select(i =>
+ query.StudioIds = studios.Select(i =>
{
try
{
@@ -758,13 +755,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="limit">The item limit.</param>
/// <param name="searchTerm">The search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <response code="200">Items returned.</response>
@@ -778,12 +775,12 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@@ -814,13 +811,13 @@ namespace Jellyfin.Api.Controllers
ParentId = parentIdGuid,
Recursive = true,
DtoOptions = dtoOptions,
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ MediaTypes = mediaTypes,
IsVirtualItem = false,
CollapseBoxSetItems = false,
EnableTotalRecordCount = enableTotalRecordCount,
AncestorIds = ancestorIds,
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
SearchTerm = searchTerm
});
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 546930440..3ff77e8e0 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -362,15 +362,14 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public ActionResult DeleteItems([FromQuery] string? ids)
+ public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids)
{
- if (string.IsNullOrEmpty(ids))
+ if (ids.Length == 0)
{
return NoContent();
}
- var itemIds = RequestHelpers.Split(ids, ',', true);
- foreach (var i in itemIds)
+ foreach (var i in ids)
{
var item = _libraryManager.GetItemById(i);
var auth = _authContext.GetAuthorizationInfo(Request);
@@ -456,7 +455,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions().AddClientFields(Request);
- BaseItem parent = item.GetParent();
+ BaseItem? parent = item.GetParent();
while (parent != null)
{
@@ -467,7 +466,7 @@ namespace Jellyfin.Api.Controllers
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
- parent = parent.GetParent();
+ parent = parent?.GetParent();
}
return baseItemDtos;
@@ -691,7 +690,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId,
- [FromQuery] string? excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
@@ -753,9 +752,9 @@ namespace Jellyfin.Api.Controllers
};
// ExcludeArtistIds
- if (!string.IsNullOrEmpty(excludeArtistIds))
+ if (excludeArtistIds.Length != 0)
{
- query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+ query.ExcludeArtistIds = excludeArtistIds;
}
List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
@@ -893,7 +892,7 @@ namespace Jellyfin.Api.Controllers
return _libraryManager.GetItemsResult(query).TotalRecordCount;
}
- private BaseItem TranslateParentItem(BaseItem item, User user)
+ private BaseItem? TranslateParentItem(BaseItem item, User user)
{
return item.GetParent() is AggregateFolder
? _libraryManager.GetUserRootFolder().GetChildren(user, true)
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 458f40b2d..410f3a340 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
@@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
- [FromQuery] string? sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true)
@@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews,
IsKids = isKids,
IsSports = isSports,
- SortBy = RequestHelpers.Split(sortBy, ',', true),
+ SortBy = sortBy,
SortOrder = sortOrder ?? SortOrder.Ascending,
AddCurrentProgram = addCurrentProgram
},
@@ -539,7 +539,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
- [FromQuery] string? channelIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId,
[FromQuery] DateTime? minStartDate,
[FromQuery] bool? hasAired,
@@ -556,8 +556,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] string? sortBy,
[FromQuery] string? sortOrder,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -573,8 +573,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ChannelIds = RequestHelpers.Split(channelIds, ',', true)
- .Select(i => new Guid(i)).ToArray(),
+ ChannelIds = channelIds,
HasAired = hasAired,
IsAiring = isAiring,
EnableTotalRecordCount = enableTotalRecordCount,
@@ -591,8 +590,8 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids,
IsSports = isSports,
SeriesTimerId = seriesTimerId,
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds)
+ Genres = genres,
+ GenreIds = genreIds
};
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
@@ -628,8 +627,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true)
- .Select(i => new Guid(i)).ToArray(),
+ ChannelIds = body.ChannelIds,
HasAired = body.HasAired,
IsAiring = body.IsAiring,
EnableTotalRecordCount = body.EnableTotalRecordCount,
@@ -646,8 +644,8 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids,
IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId,
- Genres = RequestHelpers.Split(body.Genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(body.GenreIds)
+ Genres = body.Genres,
+ GenreIds = body.GenreIds
};
if (!body.LibrarySeriesId.Equals(Guid.Empty))
@@ -703,7 +701,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
@@ -723,7 +721,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews,
IsSports = isSports,
EnableTotalRecordCount = enableTotalRecordCount,
- GenreIds = RequestHelpers.GetGuids(genreIds)
+ GenreIds = genreIds
};
var dtoOptions = new DtoOptions { Fields = fields }
@@ -1073,7 +1071,7 @@ namespace Jellyfin.Api.Controllers
var client = _httpClientFactory.CreateClient(NamedClient.Default);
// https://json.schedulesdirect.org/20141201/available/countries
// Can't dispose the response as it's required up the call chain.
- var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries")
+ var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
.ConfigureAwait(false);
return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
@@ -1155,7 +1153,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="newDevicesOnly">Only discover new tuners.</param>
/// <response code="200">Tuners returned.</response>
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
- [HttpGet("Tuners/Discvover")]
+ [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
+ [HttpGet("Tuners/Discover")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index ab9e4cf8e..8c6104302 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
var result = _libraryManager.GetMusicGenres(query);
- var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
@@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
- MusicGenre item;
+ MusicGenre? item;
if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
{
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
- private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+ private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
where T : BaseItem, new()
{
var result = libraryManager.GetItemList(new InternalItemsQuery
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index d7c6a484a..1f797d6bc 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -54,6 +54,11 @@ namespace Jellyfin.Api.Controllers
string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
.FirstOrDefault();
+ if (result == null)
+ {
+ return NotFound();
+ }
+
return result;
}
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 6ac3e6417..9dc79b388 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -77,8 +77,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? excludePersonTypes,
- [FromQuery] string? personTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? appearsInItemId,
[FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true)
@@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
+ PersonTypes = personTypes,
+ ExcludePersonTypes = excludePersonTypes,
NameContains = searchTerm,
User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 4b3d8d3d3..bc47ecbd1 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -63,11 +63,10 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromBody, Required] CreatePlaylistDto createPlaylistRequest)
{
- Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{
Name = createPlaylistRequest.Name,
- ItemIdList = idGuidArray,
+ ItemIdList = createPlaylistRequest.Ids,
UserId = createPlaylistRequest.UserId,
MediaType = createPlaylistRequest.MediaType
}).ConfigureAwait(false);
@@ -87,10 +86,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToPlaylist(
[FromRoute, Required] Guid playlistId,
- [FromQuery] string? ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] Guid? userId)
{
- await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false);
+ await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
return NoContent();
}
@@ -122,9 +121,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds)
+ public async Task<ActionResult> RemoveFromPlaylist(
+ [FromRoute, Required] string playlistId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
{
- await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false);
+ await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 331277aec..5284888d8 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -157,9 +157,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
- public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
+ public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
{
- var urlHash = imageUrl.GetMD5();
+ var urlHash = imageUrl.ToString().GetMD5();
var pointerCachePath = GetFullCachePath(urlHash.ToString());
string? contentPath = null;
@@ -245,17 +245,25 @@ namespace Jellyfin.Api.Controllers
/// <param name="urlHash">The URL hash.</param>
/// <param name="pointerCachePath">The pointer cache path.</param>
/// <returns>Task.</returns>
- private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
+ private async Task DownloadImage(Uri url, Guid urlHash, string pointerCachePath)
{
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
+ if (response.Content.Headers.ContentType?.MediaType == null)
+ {
+ throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
+ }
+
var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
- Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+ var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
+ Directory.CreateDirectory(fullCacheDirectory);
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
- Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+
+ var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
+ Directory.CreateDirectory(pointerCacheDirectory);
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
.ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 62c870cb1..076fe58f1 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -82,9 +83,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] Guid? userId,
[FromQuery, Required] string searchTerm,
- [FromQuery] string? includeItemTypes,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? parentId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
@@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers
IncludeStudios = includeStudios,
StartIndex = startIndex,
UserId = userId ?? Guid.Empty,
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ MediaTypes = mediaTypes,
ParentId = parentId,
IsKids = isKids,
@@ -260,7 +261,7 @@ namespace Jellyfin.Api.Controllers
}
}
- private T GetParentWithImage<T>(BaseItem item, ImageType type)
+ private T? GetParentWithImage<T>(BaseItem item, ImageType type)
where T : BaseItem
{
return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index e506ac7bf..6c9b9050e 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -160,12 +160,12 @@ namespace Jellyfin.Api.Controllers
public ActionResult Play(
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
- [FromQuery, Required] string itemIds,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks)
{
var playRequest = new PlayRequest
{
- ItemIds = RequestHelpers.GetGuids(itemIds),
+ ItemIds = itemIds,
StartPositionTicks = startPositionTicks,
PlayCommand = playCommand
};
@@ -378,7 +378,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities(
[FromQuery] string? id,
- [FromQuery] string? playableMediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
@@ -391,7 +391,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.ReportCapabilities(id, new ClientCapabilities
{
- PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
+ PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync,
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 27dcd51bc..af28b4f59 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -73,8 +73,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
@@ -94,13 +94,10 @@ namespace Jellyfin.Api.Controllers
var parentItem = _libraryManager.GetParentItem(parentId, userId);
- var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
- var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
-
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = excludeItemTypesArr,
- IncludeItemTypes = includeItemTypesArr,
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -125,7 +122,7 @@ namespace Jellyfin.Api.Controllers
}
var result = _libraryManager.GetStudios(query);
- var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index ad64adfba..69292186e 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -58,8 +59,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromRoute, Required] Guid userId,
- [FromQuery] string? mediaType,
- [FromQuery] string? type,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
@@ -70,8 +71,8 @@ namespace Jellyfin.Api.Controllers
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{
OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
- MediaTypes = RequestHelpers.Split(mediaType!, ',', true),
- IncludeItemTypes = RequestHelpers.Split(type!, ',', true),
+ MediaTypes = mediaType,
+ IncludeItemTypes = type,
IsVirtualItem = false,
StartIndex = startIndex,
Limit = limit,
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index ffdbb15df..5b71fed5a 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Model.Dto;
@@ -42,8 +42,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
/// <param name="isHd">Optional filter by items that are HD or not.</param>
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
@@ -56,41 +56,41 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
- /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
- /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
- /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
- /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
- /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="isLocked">Optional filter by items that are locked.</param>
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
@@ -101,12 +101,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
- /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
@@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
- [FromQuery] string? locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
@@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId,
- [FromQuery] string? excludeItemIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
@@ -147,33 +147,33 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
- [FromQuery] string? genres,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? artists,
- [FromQuery] string? excludeArtistIds,
- [FromQuery] string? artistIds,
- [FromQuery] string? albumArtistIds,
- [FromQuery] string? contributingArtistIds,
- [FromQuery] string? albums,
- [FromQuery] string? albumIds,
- [FromQuery] string? ids,
- [FromQuery] string? videoTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@@ -184,16 +184,16 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
- [FromQuery] string? seriesStatus,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery] string? studioIds,
- [FromQuery] string? genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
- var includeItemTypes = "Trailer";
+ var includeItemTypes = new[] { "Trailer" };
return _itemsController
.GetItems(
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 6fd154836..57b056f50 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="seriesId">The series id.</param>
/// <param name="userId">The user id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="season">Optional filter by season number.</param>
/// <param name="seasonId">Optional. Filter by season id.</param>
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
[HttpGet("{seriesId}/Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="seriesId">The series id.</param>
/// <param name="userId">The user id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="isSpecialSeason">Optional. Filter by special season.</param>
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index e10f1fe91..255532307 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
@@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
@@ -191,8 +192,11 @@ namespace Jellyfin.Api.Controllers
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
+ // ffmpeg option -> file extension
+ // mpegts -> ts
+ // fmp4 -> mp4
// TODO: remove this when we switch back to the segment muxer
- var supportedHlsContainers = new[] { "mpegts", "fmp4" };
+ var supportedHlsContainers = new[] { "ts", "mp4" };
var dynamicHlsRequestDto = new HlsAudioRequestDto
{
@@ -201,7 +205,7 @@ namespace Jellyfin.Api.Controllers
Static = isStatic,
PlaySessionId = info.PlaySessionId,
// fallback to mpegts if device reports some weird value unsupported by hls
- SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
+ SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
AudioCodec = audioCodec,
@@ -258,7 +262,7 @@ namespace Jellyfin.Api.Controllers
}
private DeviceProfile GetDeviceProfile(
- string? container,
+ string[] containers,
string? transcodingContainer,
string? audioCodec,
string? transcodingProtocol,
@@ -270,7 +274,6 @@ namespace Jellyfin.Api.Controllers
{
var deviceProfile = new DeviceProfile();
- var containers = RequestHelpers.Split(container, ',', true);
int len = containers.Length;
var directPlayProfiles = new DirectPlayProfile[len];
for (int i = 0; i < len; i++)
@@ -327,7 +330,7 @@ namespace Jellyfin.Api.Controllers
if (conditions.Count > 0)
{
// codec profile
- codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() });
+ codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() });
}
deviceProfile.CodecProfiles = codecProfiles.ToArray();
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index cfd851129..0e65591cc 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -253,7 +253,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">User id.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="isPlayed">Filter by items that are played, or not.</param>
/// <param name="enableImages">Optional. include image information in output.</param>
/// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
@@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
@@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
new LatestItemsQuery
{
GroupItems = groupItems,
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ IncludeItemTypes = includeItemTypes,
IsPlayed = isPlayed,
Limit = limit,
ParentId = parentId ?? Guid.Empty,
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index d575bfc3b..60fd1df01 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
- [FromQuery] string? presetViews,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
[FromQuery] bool includeHidden = false)
{
var query = new UserViewQuery
@@ -81,9 +82,9 @@ namespace Jellyfin.Api.Controllers
query.IncludeExternalContent = includeExternalContent.Value;
}
- if (!string.IsNullOrWhiteSpace(presetViews))
+ if (presetViews.Length != 0)
{
- query.PresetViews = RequestHelpers.Split(presetViews, ',', true);
+ query.PresetViews = presetViews;
}
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index d7bcf79c1..2ac16de6b 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -1,8 +1,9 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -11,6 +12,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
@@ -144,7 +146,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -295,23 +297,23 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false);
TranscodingJobDto? job = null;
- var playlist = state.OutputFilePath;
+ var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
- if (!System.IO.File.Exists(playlist))
+ if (!System.IO.File.Exists(playlistPath))
{
- var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlist);
+ var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
- if (!System.IO.File.Exists(playlist))
+ if (!System.IO.File.Exists(playlistPath))
{
// If the playlist doesn't already exist, startup ffmpeg
try
{
job = await _transcodingJobHelper.StartFfMpeg(
state,
- playlist,
- GetCommandLineArguments(playlist, state),
+ playlistPath,
+ GetCommandLineArguments(playlistPath, state),
Request,
TranscodingJobType,
cancellationTokenSource)
@@ -327,7 +329,7 @@ namespace Jellyfin.Api.Controllers
minSegments = state.MinSegments;
if (minSegments > 0)
{
- await HlsHelpers.WaitForMinimumSegmentCount(playlist, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
+ await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
}
}
}
@@ -337,14 +339,14 @@ namespace Jellyfin.Api.Controllers
}
}
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlist, TranscodingJobType);
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job != null)
{
_transcodingJobHelper.OnTranscodeEndRequest(job);
}
- var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength);
+ var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
}
@@ -358,16 +360,46 @@ namespace Jellyfin.Api.Controllers
private string GetCommandLineArguments(string outputPath, StreamState state)
{
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
+ var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static.
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
- var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
- var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
+ var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
- var segmentFormat = format.TrimStart('.');
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+ var outputTsArg = outputPrefix + "%d" + outputExtension;
+
+ var segmentFormat = outputExtension.TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
+ else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var outputFmp4HeaderArg = string.Empty;
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ if (isWindows)
+ {
+ // on Windows, the path of fmp4 header file needs to be configured
+ outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
+ }
+ else
+ {
+ // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
+ outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
+ }
+
+ segmentFormat = "fmp4" + outputFmp4HeaderArg;
+ }
+ else
+ {
+ _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat);
+ }
+
+ var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
+ ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+ : "128";
var baseUrlParam = string.Format(
CultureInfo.InvariantCulture,
@@ -376,20 +408,19 @@ namespace Jellyfin.Api.Controllers
return string.Format(
CultureInfo.InvariantCulture,
- "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {7} -individual_header_trailer 0 -segment_format {8} -segment_list_entry_prefix {9} -segment_list_type m3u8 -segment_start_number 0 -segment_list \"{10}\" -y \"{11}\"",
+ "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"",
inputModifier,
_encodingHelper.GetInputArgument(state, _encodingOptions),
threads,
- _encodingHelper.GetMapArgs(state),
+ mapArgs,
GetVideoArguments(state),
GetAudioArguments(state),
+ maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
- string.Empty,
segmentFormat,
baseUrlParam,
- outputPath,
- outputTsArg)
- .Trim();
+ outputTsArg,
+ outputPath).Trim();
}
/// <summary>
@@ -399,14 +430,53 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for audio transcoding.</returns>
private string GetAudioArguments(StreamState state)
{
- var codec = _encodingHelper.GetAudioEncoder(state);
+ if (state.AudioStream == null)
+ {
+ return string.Empty;
+ }
- if (EncodingHelper.IsCopyCodec(codec))
+ var audioCodec = _encodingHelper.GetAudioEncoder(state);
+
+ if (!state.IsOutputVideo)
+ {
+ if (EncodingHelper.IsCopyCodec(audioCodec))
+ {
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+ return "-acodec copy -strict -2" + bitStreamArgs;
+ }
+
+ var audioTranscodeParams = string.Empty;
+
+ audioTranscodeParams += "-acodec " + audioCodec;
+
+ if (state.OutputAudioBitrate.HasValue)
+ {
+ audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ if (state.OutputAudioChannels.HasValue)
+ {
+ audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ audioTranscodeParams += " -vn";
+ return audioTranscodeParams;
+ }
+
+ if (EncodingHelper.IsCopyCodec(audioCodec))
{
- return "-codec:a:0 copy";
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+ return "-acodec copy -strict -2" + bitStreamArgs;
}
- var args = "-codec:a:0 " + codec;
+ var args = "-codec:a:0 " + audioCodec;
var channels = state.OutputAudioChannels;
@@ -427,7 +497,7 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
+ args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args;
}
@@ -439,6 +509,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for video transcoding.</returns>
private string GetVideoArguments(StreamState state)
{
+ if (state.VideoStream == null)
+ {
+ return string.Empty;
+ }
+
if (!state.IsOutputVideo)
{
return string.Empty;
@@ -448,47 +523,65 @@ namespace Jellyfin.Api.Controllers
var args = "-codec:v:0 " + codec;
+ // Prefer hvc1 to hev1.
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ args += " -tag:v:0 hvc1";
+ }
+
// if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
- // See if we can save come cpu cycles by avoiding encoding
- if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+ // See if we can save come cpu cycles by avoiding encoding.
+ if (EncodingHelper.IsCopyCodec(codec))
{
- // if h264_mp4toannexb is ever added, do not use it for live tv
- if (state.VideoStream != null &&
- !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+ // If h264_mp4toannexb is ever added, do not use it for live tv.
+ if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+ string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;
}
}
+
+ args += " -start_at_zero";
}
else
{
- var keyFrameArg = string.Format(
- CultureInfo.InvariantCulture,
- " -force_key_frames \"expr:gte(t,n_forced*{0})\"",
- state.SegmentLength.ToString(CultureInfo.InvariantCulture));
+ args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
- var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ // Set the key frame params for video encoding to match the hls segment time.
+ args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null);
- args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg;
-
- // Add resolution params, if specified
- if (!hasGraphicalSubs)
+ // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+ if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
- args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
+ args += " -bf 0";
}
- // This is for internal graphical subs
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
if (hasGraphicalSubs)
{
+ // Graphical subs overlay and resolution params.
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
}
+ else
+ {
+ // Resolution params.
+ args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
+ }
+
+ if (state.SubtitleStream == null || !state.SubtitleStream.IsExternal || state.SubtitleStream.IsTextSubtitleStream)
+ {
+ args += " -start_at_zero";
+ }
}
args += " -flags -global_header";
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 1a23076f1..8e17b843a 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -10,6 +10,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
@@ -203,9 +204,9 @@ 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds)
{
- var items = RequestHelpers.Split(itemIds, ',', true)
+ var items = itemIds
.Select(i => _libraryManager.GetItemById(i))
.OfType<Video>()
.OrderBy(i => i.Id)
@@ -283,7 +284,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -312,7 +313,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 1b38e399d..9c3ecb4ce 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -73,9 +73,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder,
[FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
@@ -103,19 +103,15 @@ namespace Jellyfin.Api.Controllers
IList<BaseItem> items;
- var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
- var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
- var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
-
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = excludeItemTypesArr,
- IncludeItemTypes = includeItemTypesArr,
- MediaTypes = mediaTypesArr,
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
DtoOptions = dtoOptions
};
- bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr);
+ bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
if (parentItem.IsFolder)
{
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index a3f2d88ce..21ec2d32f 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -1,8 +1,10 @@
-using System.Net.Http;
+using System;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -98,6 +100,11 @@ namespace Jellyfin.Api.Helpers
TranscodingJobType transcodingJobType,
StreamingRequestDto streamingRequest)
{
+ if (_httpContextAccessor.HttpContext == null)
+ {
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+ }
+
bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index ea012f837..a4da54cfd 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Helpers
StreamingRequestDto streamingRequest,
bool enableAdaptiveBitrateStreaming)
{
- var isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == WebRequestMethods.Http.Head;
+ var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
return await GetMasterPlaylistInternal(
streamingRequest,
@@ -130,6 +130,11 @@ namespace Jellyfin.Api.Helpers
TranscodingJobType transcodingJobType,
CancellationTokenSource cancellationTokenSource)
{
+ if (_httpContextAccessor.HttpContext == null)
+ {
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+ }
+
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
_httpContextAccessor.HttpContext.Request,
@@ -202,7 +207,61 @@ namespace Jellyfin.Api.Helpers
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
}
- AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+ var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+
+ if (state.VideoStream != null && state.VideoRequest != null)
+ {
+ // Provide SDR HEVC entrance for backward compatibility.
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+ && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
+ if (requestedVideoProfiles != null && requestedVideoProfiles.Length > 0)
+ {
+ // Force HEVC Main Profile and disable video stream copy.
+ state.OutputVideoCodec = "hevc";
+ var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(",", requestedVideoProfiles), "main");
+ sdrVideoUrl += "&AllowVideoStreamCopy=false";
+
+ EncodingHelper encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+ var sdrOutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
+ var sdrOutputAudioBitrate = encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
+ var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
+
+ AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
+
+ // Restore the video codec
+ state.OutputVideoCodec = "copy";
+ }
+ }
+
+ // Provide Level 5.0 entrance for backward compatibility.
+ // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
+ // but in fact it is capable of playing videos up to Level 6.1.
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream.Level.HasValue
+ && state.VideoStream.Level > 150
+ && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+ && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ var playlistCodecsField = new StringBuilder();
+ AppendPlaylistCodecsField(playlistCodecsField, state);
+
+ // Force the video level to 5.0.
+ var originalLevel = state.VideoStream.Level;
+ state.VideoStream.Level = 150;
+ var newPlaylistCodecsField = new StringBuilder();
+ AppendPlaylistCodecsField(newPlaylistCodecsField, state);
+
+ // Restore the video level.
+ state.VideoStream.Level = originalLevel;
+ var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
+ builder.Append(newPlaylist);
+ }
+ }
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
{
@@ -212,40 +271,77 @@ namespace Jellyfin.Api.Helpers
var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation;
- var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2;
newBitrate = totalBitrate - variation;
- variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
- private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+ private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
{
- builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+ var playlistBuilder = new StringBuilder();
+ playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
.Append(",AVERAGE-BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
- AppendPlaylistCodecsField(builder, state);
+ AppendPlaylistVideoRangeField(playlistBuilder, state);
+
+ AppendPlaylistCodecsField(playlistBuilder, state);
- AppendPlaylistResolutionField(builder, state);
+ AppendPlaylistResolutionField(playlistBuilder, state);
- AppendPlaylistFramerateField(builder, state);
+ AppendPlaylistFramerateField(playlistBuilder, state);
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
- builder.Append(",SUBTITLES=\"")
+ playlistBuilder.Append(",SUBTITLES=\"")
.Append(subtitleGroup)
.Append('"');
}
- builder.Append(Environment.NewLine);
- builder.AppendLine(url);
+ playlistBuilder.Append(Environment.NewLine);
+ playlistBuilder.AppendLine(url);
+ builder.Append(playlistBuilder);
+
+ return playlistBuilder;
+ }
+
+ /// <summary>
+ /// Appends a VIDEO-RANGE field containing the range of the output video stream.
+ /// </summary>
+ /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="state">StreamState of the current stream.</param>
+ private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
+ {
+ if (state.VideoStream != null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
+ {
+ var videoRange = state.VideoStream.VideoRange;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ {
+ if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
+ {
+ builder.Append(",VIDEO-RANGE=SDR");
+ }
+
+ if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ builder.Append(",VIDEO-RANGE=PQ");
+ }
+ }
+ else
+ {
+ // Currently we only encode to SDR.
+ builder.Append(",VIDEO-RANGE=SDR");
+ }
+ }
}
/// <summary>
@@ -414,15 +510,27 @@ namespace Jellyfin.Api.Helpers
/// <returns>H.26X level of the output video stream.</returns>
private int? GetOutputVideoCodecLevel(StreamState state)
{
- string? levelString;
+ string levelString = string.Empty;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream != null
&& state.VideoStream.Level.HasValue)
{
- levelString = state.VideoStream?.Level.ToString();
+ levelString = state.VideoStream.Level.ToString() ?? string.Empty;
}
else
{
- levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
+ if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
+ }
+
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
+ }
}
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
@@ -434,6 +542,38 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
+ /// Get the H.26X profile of the output video stream.
+ /// </summary>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <param name="codec">Video codec.</param>
+ /// <returns>H.26X profile of the output video stream.</returns>
+ private string GetOutputVideoCodecProfile(StreamState state, string codec)
+ {
+ string profileString = string.Empty;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrEmpty(state.VideoStream.Profile))
+ {
+ profileString = state.VideoStream.Profile;
+ }
+ else if (!string.IsNullOrEmpty(codec))
+ {
+ profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
+ if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ profileString = profileString ?? "high";
+ }
+
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ profileString = profileString ?? "main";
+ }
+ }
+
+ return profileString;
+ }
+
+ /// <summary>
/// Gets a formatted string of the output audio codec, for use in the CODECS field.
/// </summary>
/// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
@@ -463,6 +603,16 @@ namespace Jellyfin.Api.Helpers
return HlsCodecStringHelpers.GetEAC3String();
}
+ if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetFLACString();
+ }
+
+ if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ return HlsCodecStringHelpers.GetALACString();
+ }
+
return string.Empty;
}
@@ -487,15 +637,14 @@ namespace Jellyfin.Api.Helpers
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{
- string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+ string profile = GetOutputVideoCodecProfile(state, "h264");
return HlsCodecStringHelpers.GetH264String(profile, level);
}
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
-
+ string profile = GetOutputVideoCodecProfile(state, "hevc");
return HlsCodecStringHelpers.GetH265String(profile, level);
}
@@ -539,12 +688,30 @@ namespace Jellyfin.Api.Helpers
return variation;
}
- private string ReplaceBitrate(string url, int oldValue, int newValue)
+ private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase);
}
+
+ private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
+ {
+ string profileStr = codec + "-profile=";
+ return url.Replace(
+ profileStr + oldValue,
+ profileStr + newValue,
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
+ {
+ var oldPlaylist = playlist.ToString();
+ return oldPlaylist.Replace(
+ oldValue.ToString(),
+ newValue.ToString(),
+ StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 366301d3e..cfa2c1229 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -24,12 +24,14 @@ namespace Jellyfin.Api.Helpers
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
/// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
/// <param name="httpContext">The current http context.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
public static async Task<ActionResult> GetStaticRemoteStreamResult(
StreamState state,
bool isHeadRequest,
HttpClient httpClient,
- HttpContext httpContext)
+ HttpContext httpContext,
+ CancellationToken cancellationToken = default)
{
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
{
@@ -37,8 +39,8 @@ namespace Jellyfin.Api.Helpers
}
// Can't dispose the response as it's required up the call chain.
- var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType.ToString();
+ var response = await httpClient.GetAsync(new Uri(state.MediaPath)).ConfigureAwait(false);
+ var contentType = response.Content.Headers.ContentType?.ToString();
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
@@ -47,7 +49,7 @@ namespace Jellyfin.Api.Helpers
return new FileContentResult(Array.Empty<byte>(), contentType);
}
- return new FileStreamResult(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
+ return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
}
/// <summary>
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 95f1906ef..a5369c441 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -10,12 +10,37 @@ namespace Jellyfin.Api.Helpers
public static class HlsCodecStringHelpers
{
/// <summary>
+ /// Codec name for MP3.
+ /// </summary>
+ public const string MP3 = "mp4a.40.34";
+
+ /// <summary>
+ /// Codec name for AC-3.
+ /// </summary>
+ public const string AC3 = "mp4a.a5";
+
+ /// <summary>
+ /// Codec name for E-AC-3.
+ /// </summary>
+ public const string EAC3 = "mp4a.a6";
+
+ /// <summary>
+ /// Codec name for FLAC.
+ /// </summary>
+ public const string FLAC = "fLaC";
+
+ /// <summary>
+ /// Codec name for ALAC.
+ /// </summary>
+ public const string ALAC = "alac";
+
+ /// <summary>
/// Gets a MP3 codec string.
/// </summary>
/// <returns>MP3 codec string.</returns>
public static string GetMP3String()
{
- return "mp4a.40.34";
+ return MP3;
}
/// <summary>
@@ -23,7 +48,7 @@ namespace Jellyfin.Api.Helpers
/// </summary>
/// <param name="profile">AAC profile.</param>
/// <returns>AAC codec string.</returns>
- public static string GetAACString(string profile)
+ public static string GetAACString(string? profile)
{
StringBuilder result = new StringBuilder("mp4a", 9);
@@ -41,12 +66,48 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
+ /// Gets an AC-3 codec string.
+ /// </summary>
+ /// <returns>AC-3 codec string.</returns>
+ public static string GetAC3String()
+ {
+ return AC3;
+ }
+
+ /// <summary>
+ /// Gets an E-AC-3 codec string.
+ /// </summary>
+ /// <returns>E-AC-3 codec string.</returns>
+ public static string GetEAC3String()
+ {
+ return EAC3;
+ }
+
+ /// <summary>
+ /// Gets an FLAC codec string.
+ /// </summary>
+ /// <returns>FLAC codec string.</returns>
+ public static string GetFLACString()
+ {
+ return FLAC;
+ }
+
+ /// <summary>
+ /// Gets an ALAC codec string.
+ /// </summary>
+ /// <returns>ALAC codec string.</returns>
+ public static string GetALACString()
+ {
+ return ALAC;
+ }
+
+ /// <summary>
/// Gets a H.264 codec string.
/// </summary>
/// <param name="profile">H.264 profile.</param>
/// <param name="level">H.264 level.</param>
/// <returns>H.264 string.</returns>
- public static string GetH264String(string profile, int level)
+ public static string GetH264String(string? profile, int level)
{
StringBuilder result = new StringBuilder("avc1", 11);
@@ -80,46 +141,29 @@ namespace Jellyfin.Api.Helpers
/// <param name="profile">H.265 profile.</param>
/// <param name="level">H.265 level.</param>
/// <returns>H.265 string.</returns>
- public static string GetH265String(string profile, int level)
+ public static string GetH265String(string? profile, int level)
{
// The h265 syntax is a bit of a mystery at the time this comment was written.
// This is what I've found through various sources:
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
- StringBuilder result = new StringBuilder("hev1", 16);
+ StringBuilder result = new StringBuilder("hvc1", 16);
- if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
{
- result.Append(".2.6");
+ result.Append(".2.4");
}
else
{
// Default to main if profile is invalid
- result.Append(".1.6");
+ result.Append(".1.4");
}
result.Append(".L")
- .Append(level * 3)
+ .Append(level)
.Append(".B0");
return result.ToString();
}
-
- /// <summary>
- /// Gets an AC-3 codec string.
- /// </summary>
- /// <returns>AC-3 codec string.</returns>
- public static string GetAC3String()
- {
- return "mp4a.a5";
- }
-
- /// <summary>
- /// Gets an E-AC-3 codec string.
- /// </summary>
- /// <returns>E-AC-3 codec string.</returns>
- public static string GetEAC3String()
- {
- return "mp4a.a6";
- }
}
}
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index 242496697..18e23fb5c 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -1,8 +1,11 @@
using System;
using System.Globalization;
using System.IO;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -45,6 +48,11 @@ namespace Jellyfin.Api.Helpers
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync().ConfigureAwait(false);
+ if (line == null)
+ {
+ // Nothing currently in buffer.
+ break;
+ }
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
{
@@ -70,24 +78,64 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
+ /// Gets the #EXT-X-MAP string.
+ /// </summary>
+ /// <param name="outputPath">The output path of the file.</param>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <param name="isOsDepends">Get a normal string or depends on OS.</param>
+ /// <returns>The string text of #EXT-X-MAP.</returns>
+ public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends)
+ {
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+
+ // on Linux/Unix
+ // #EXT-X-MAP:URI="prefix-1.mp4"
+ var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension;
+ if (!isOsDepends)
+ {
+ return fmp4InitFileName;
+ }
+
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ if (isWindows)
+ {
+ // on Windows
+ // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
+ fmp4InitFileName = outputPrefix + "-1" + outputExtension;
+ }
+
+ return fmp4InitFileName;
+ }
+
+ /// <summary>
/// Gets the hls playlist text.
/// </summary>
/// <param name="path">The path to the playlist file.</param>
- /// <param name="segmentLength">The segment length.</param>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
/// <returns>The playlist text as a string.</returns>
- public static string GetLivePlaylistText(string path, int segmentLength)
+ public static string GetLivePlaylistText(string path, StreamState state)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
- text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT", StringComparison.InvariantCulture);
-
- var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
+ var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
+ if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var fmp4InitFileName = GetFmp4InitFileName(path, state, true);
+ var baseUrlParam = string.Format(
+ CultureInfo.InvariantCulture,
+ "hls/{0}/",
+ Path.GetFileNameWithoutExtension(path));
+ var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false);
- text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
- // text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
+ // Replace fMP4 init file URI.
+ text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture);
+ }
return text;
}
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index e00ed3304..8bddf00d5 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
@@ -90,6 +91,11 @@ namespace Jellyfin.Api.Helpers
allowAsyncFileRead = true;
}
+ if (_path == null)
+ {
+ throw new ResourceNotFoundException(nameof(_path));
+ }
+
await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
var eofCount = 0;
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index f06f038ab..efce11f8a 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -122,49 +122,6 @@ namespace Jellyfin.Api.Helpers
return session;
}
- /// <summary>
- /// Get Guid array from string.
- /// </summary>
- /// <param name="value">String value.</param>
- /// <returns>Guid array.</returns>
- internal static Guid[] GetGuids(string? value)
- {
- if (value == null)
- {
- return Array.Empty<Guid>();
- }
-
- return Split(value, ',', true)
- .Select(i => new Guid(i))
- .ToArray();
- }
-
- /// <summary>
- /// Gets the item fields.
- /// </summary>
- /// <param name="fields">The fields string.</param>
- /// <returns>IEnumerable{ItemFields}.</returns>
- internal static ItemFields[] GetItemFields(string? fields)
- {
- if (string.IsNullOrEmpty(fields))
- {
- return Array.Empty<ItemFields>();
- }
-
- return Split(fields, ',', true)
- .Select(v =>
- {
- if (Enum.TryParse(v, true, out ItemFields value))
- {
- return (ItemFields?)value;
- }
-
- return null;
- }).Where(i => i.HasValue)
- .Select(i => i!.Value)
- .ToArray();
- }
-
internal static QueryResult<BaseItemDto> CreateQueryResult(
QueryResult<(BaseItem, ItemCounts)> result,
DtoOptions dtoOptions,
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 4e6e981be..c6d844c4f 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -83,6 +83,10 @@ namespace Jellyfin.Api.Helpers
}
streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
+ if (httpRequest.Path.Value == null)
+ {
+ throw new ResourceNotFoundException(nameof(httpRequest.Path));
+ }
var url = httpRequest.Path.Value.Split('.')[^1];
@@ -165,7 +169,9 @@ namespace Jellyfin.Api.Helpers
state.DirectStreamProvider = liveStreamInfo.Item2;
}
- encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
+ var encodingOptions = serverConfigurationManager.GetEncodingOptions();
+
+ encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
string? containerInternal = Path.GetExtension(state.RequestedUrl);
@@ -183,7 +189,7 @@ namespace Jellyfin.Api.Helpers
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
- state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
+ state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
state.OutputAudioCodec = streamingRequest.AudioCodec;
@@ -196,20 +202,41 @@ namespace Jellyfin.Api.Helpers
encodingHelper.TryStreamCopy(state);
- if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+ if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
{
- var resolution = ResolutionNormalizer.Normalize(
- state.VideoStream?.BitRate,
- state.VideoStream?.Width,
- state.VideoStream?.Height,
- state.OutputVideoBitrate.Value,
- state.VideoStream?.Codec,
- state.OutputVideoCodec,
- state.VideoRequest.MaxWidth,
- state.VideoRequest.MaxHeight);
-
- state.VideoRequest.MaxWidth = resolution.MaxWidth;
- state.VideoRequest.MaxHeight = resolution.MaxHeight;
+ var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
+ && !state.VideoRequest.Height.HasValue
+ && !state.VideoRequest.MaxWidth.HasValue
+ && !state.VideoRequest.MaxHeight.HasValue;
+
+ if (isVideoResolutionNotRequested
+ && state.VideoRequest.VideoBitRate.HasValue
+ && state.VideoStream.BitRate.HasValue
+ && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
+ {
+ // Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
+ // and the requested video bitrate is higher than source video bitrate.
+ if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
+ {
+ state.VideoRequest.MaxWidth = state.VideoStream?.Width;
+ state.VideoRequest.MaxHeight = state.VideoStream?.Height;
+ }
+ }
+ else
+ {
+ var resolution = ResolutionNormalizer.Normalize(
+ state.VideoStream?.BitRate,
+ state.VideoStream?.Width,
+ state.VideoStream?.Height,
+ state.OutputVideoBitrate.Value,
+ state.VideoStream?.Codec,
+ state.OutputVideoCodec,
+ state.VideoRequest.MaxWidth,
+ state.VideoRequest.MaxHeight);
+
+ state.VideoRequest.MaxWidth = resolution.MaxWidth;
+ state.VideoRequest.MaxHeight = resolution.MaxHeight;
+ }
}
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 0db1fabff..99c90c315 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -12,6 +12,7 @@ using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -102,7 +103,7 @@ namespace Jellyfin.Api.Helpers
/// </summary>
/// <param name="playSessionId">Playback session id.</param>
/// <returns>The transcoding job.</returns>
- public TranscodingJobDto GetTranscodingJob(string playSessionId)
+ public TranscodingJobDto? GetTranscodingJob(string playSessionId)
{
lock (_activeTranscodingJobs)
{
@@ -116,7 +117,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="path">Path to the transcoding file.</param>
/// <param name="type">The <see cref="TranscodingJobType"/>.</param>
/// <returns>The transcoding job.</returns>
- public TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
+ public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type)
{
lock (_activeTranscodingJobs)
{
@@ -144,7 +145,7 @@ namespace Jellyfin.Api.Helpers
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
- // Progressive streams can stop on their own reliably
+ // Progressive streams can stop on their own reliably.
jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
}
@@ -193,10 +194,9 @@ namespace Jellyfin.Api.Helpers
/// Called when [transcode kill timer stopped].
/// </summary>
/// <param name="state">The state.</param>
- private async void OnTranscodeKillTimerStopped(object state)
+ private async void OnTranscodeKillTimerStopped(object? state)
{
- var job = (TranscodingJobDto)state;
-
+ var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state));
if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
{
var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
@@ -241,7 +241,7 @@ namespace Jellyfin.Api.Helpers
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
- // Progressive streams can stop on their own reliably
+ // Progressive streams can stop on their own reliably.
jobs.AddRange(_activeTranscodingJobs.Where(killJob));
}
@@ -304,10 +304,10 @@ namespace Jellyfin.Api.Helpers
process!.StandardInput.WriteLine("q");
- // Need to wait because killing is asynchronous
+ // Need to wait because killing is asynchronous.
if (!process.WaitForExit(5000))
{
- _logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
+ _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
process.Kill();
}
}
@@ -470,11 +470,11 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
- /// Starts the FFMPEG.
+ /// Starts FFmpeg.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="outputPath">The output path.</param>
- /// <param name="commandLineArguments">The command line arguments for ffmpeg.</param>
+ /// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
/// <param name="request">The <see cref="HttpRequest"/>.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param>
@@ -489,7 +489,8 @@ namespace Jellyfin.Api.Helpers
CancellationTokenSource cancellationTokenSource,
string? workingDirectory = null)
{
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ Directory.CreateDirectory(directory);
await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
@@ -500,13 +501,13 @@ namespace Jellyfin.Api.Helpers
{
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
- throw new ArgumentException("User does not have access to video transcoding");
+ throw new ArgumentException("User does not have access to video transcoding.");
}
}
if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
{
- throw new ArgumentException("FFMPEG path not set.");
+ throw new ArgumentException("FFmpeg path not set.");
}
var process = new Process
@@ -523,7 +524,7 @@ namespace Jellyfin.Api.Helpers
RedirectStandardInput = true,
FileName = _mediaEncoder.EncoderPath,
Arguments = commandLineArguments,
- WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
+ WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
ErrorDialog = false
},
EnableRaisingEvents = true
@@ -543,18 +544,20 @@ namespace Jellyfin.Api.Helpers
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
_logger.LogInformation(commandLineLogMessage);
- var logFilePrefix = "ffmpeg-transcode";
+ var logFilePrefix = "FFmpeg.Transcode-";
if (state.VideoRequest != null
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
- ? "ffmpeg-remux"
- : "ffmpeg-directstream";
+ ? "FFmpeg.Remux-"
+ : "FFmpeg.DirectStream-";
}
- var logFilePath = Path.Combine(_serverConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
+ var logFilePath = Path.Combine(
+ _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
+ $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
- // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+ // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
@@ -568,20 +571,20 @@ namespace Jellyfin.Api.Helpers
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error starting ffmpeg");
+ _logger.LogError(ex, "Error starting FFmpeg");
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
throw;
}
- _logger.LogDebug("Launched ffmpeg process");
+ _logger.LogDebug("Launched FFmpeg process");
state.TranscodingJob = transcodingJob;
- // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
+ // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
- // Wait for the file to exist before proceeeding
+ // Wait for the file to exist before proceeding
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
_logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
@@ -747,11 +750,11 @@ namespace Jellyfin.Api.Helpers
if (process.ExitCode == 0)
{
- _logger.LogInformation("FFMpeg exited with code 0");
+ _logger.LogInformation("FFmpeg exited with code 0");
}
else
{
- _logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
+ _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
}
process.Dispose();
@@ -770,8 +773,9 @@ namespace Jellyfin.Api.Helpers
new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
cancellationTokenSource.Token)
.ConfigureAwait(false);
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- _encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
+ _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
if (state.VideoRequest != null)
{
@@ -827,7 +831,7 @@ namespace Jellyfin.Api.Helpers
{
lock (_transcodingLocks)
{
- if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
+ if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
{
result = new SemaphoreSlim(1, 1);
_transcodingLocks[outputPath] = result;
@@ -837,7 +841,7 @@ namespace Jellyfin.Api.Helpers
}
}
- private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
{
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index da0852ceb..da6e5fa2d 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -6,17 +6,19 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
+ <NoWarn>AD0001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.9" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
</ItemGroup>
diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
new file mode 100644
index 000000000..a42e0e4da
--- /dev/null
+++ b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.ModelBinders
+{
+ /// <summary>
+ /// Comma delimited array model binder.
+ /// Returns an empty array of specified type if there is no query parameter.
+ /// </summary>
+ public class PipeDelimitedArrayModelBinder : IModelBinder
+ {
+ private readonly ILogger<PipeDelimitedArrayModelBinder> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PipeDelimitedArrayModelBinder"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedArrayModelBinder}"/> interface.</param>
+ public PipeDelimitedArrayModelBinder(ILogger<PipeDelimitedArrayModelBinder> logger)
+ {
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
+ var converter = TypeDescriptor.GetConverter(elementType);
+
+ if (valueProviderResult.Length > 1)
+ {
+ var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
+ bindingContext.Result = ModelBindingResult.Success(typedValues);
+ }
+ else
+ {
+ var value = valueProviderResult.FirstValue;
+
+ if (value != null)
+ {
+ var splitValues = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
+ var typedValues = GetParsedResult(splitValues, elementType, converter);
+ bindingContext.Result = ModelBindingResult.Success(typedValues);
+ }
+ else
+ {
+ var emptyResult = Array.CreateInstance(elementType, 0);
+ bindingContext.Result = ModelBindingResult.Success(emptyResult);
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
+ {
+ var parsedValues = new object?[values.Count];
+ var convertedCount = 0;
+ for (var i = 0; i < values.Count; i++)
+ {
+ try
+ {
+ parsedValues[i] = converter.ConvertFromString(values[i].Trim());
+ convertedCount++;
+ }
+ catch (FormatException e)
+ {
+ _logger.LogWarning(e, "Error converting value.");
+ }
+ }
+
+ var typedValues = Array.CreateInstance(elementType, convertedCount);
+ var typedValueIndex = 0;
+ for (var i = 0; i < parsedValues.Length; i++)
+ {
+ if (parsedValues[i] != null)
+ {
+ typedValues.SetValue(parsedValues[i], typedValueIndex);
+ typedValueIndex++;
+ }
+ }
+
+ return typedValues;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 5ca4408d1..a47ae926c 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -16,7 +16,8 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// <summary>
/// Gets or sets the channels to return guide information for.
/// </summary>
- public string? ChannelIds { get; set; }
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>();
/// <summary>
/// Gets or sets optional. Filter by user id.
@@ -115,12 +116,14 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// <summary>
/// Gets or sets the genres to return guide information for.
/// </summary>
- public string? Genres { get; set; }
+ [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
+ public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the genre ids to return guide information for.
/// </summary>
- public string? GenreIds { get; set; }
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>();
/// <summary>
/// Gets or sets include image information in output.
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index b9507a4e5..9edc19bb6 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// Start kill timer.
/// </summary>
/// <param name="callback">Callback action.</param>
- public void StartKillTimer(Action<object> callback)
+ public void StartKillTimer(Action<object?> callback)
{
StartKillTimer(callback, PingTimeout);
}
@@ -206,7 +206,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// </summary>
/// <param name="callback">Callback action.</param>
/// <param name="intervalMs">Callback interval.</param>
- public void StartKillTimer(Action<object> callback, int intervalMs)
+ public void StartKillTimer(Action<object?> callback, int intervalMs)
{
if (HasExited)
{
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
index b5e42ea29..872a46824 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
@@ -101,7 +101,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
return _config.GetConfiguration<EncodingOptions>("encoding");
}
- private async void TimerCallback(object state)
+ private async void TimerCallback(object? state)
{
if (_job.HasExited)
{
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index 0d67c86f7..d0d6889fc 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using MediaBrowser.Common.Json.Converters;
namespace Jellyfin.Api.Models.PlaylistDtos
{
@@ -15,7 +18,8 @@ namespace Jellyfin.Api.Models.PlaylistDtos
/// <summary>
/// Gets or sets item ids to add to the playlist.
/// </summary>
- public string? Ids { get; set; }
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>();
/// <summary>
/// Gets or sets the user id.
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 77d55828d..ce5465116 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -56,7 +56,7 @@ namespace Jellyfin.Api.WebSocketListeners
base.Dispose(dispose);
}
- private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+ private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{
SendData(true);
}
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index 80314b923..94df23e56 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -64,19 +64,19 @@ namespace Jellyfin.Api.WebSocketListeners
base.Dispose(dispose);
}
- private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+ private void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
{
SendData(true);
e.Task.TaskProgress -= OnTaskProgress;
}
- private void OnTaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
+ private void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
{
SendData(true);
e.Argument.TaskProgress += OnTaskProgress;
}
- private void OnTaskProgress(object sender, GenericEventArgs<double> e)
+ private void OnTaskProgress(object? sender, GenericEventArgs<double> e)
{
SendData(false);
}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 1cf43a005..d996ac69f 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -66,37 +66,37 @@ namespace Jellyfin.Api.WebSocketListeners
base.Dispose(dispose);
}
- private async void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
+ private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
{
await SendData(false).ConfigureAwait(false);
}
- private async void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e)
+ private async void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
{
await SendData(true).ConfigureAwait(false);
}
- private async void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private async void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
await SendData(!e.IsAutomated).ConfigureAwait(false);
}
- private async void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
+ private async void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
{
await SendData(true).ConfigureAwait(false);
}
- private async void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
+ private async void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
{
await SendData(true).ConfigureAwait(false);
}
- private async void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
+ private async void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
{
await SendData(true).ConfigureAwait(false);
}
- private async void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
+ private async void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
{
await SendData(true).ConfigureAwait(false);
}
diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
index 4467c9bbd..f9539964d 100644
--- a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
+++ b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
@@ -73,7 +73,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Gets or sets the next item in the collection.
/// </summary>
/// <remarks>
- /// TODO check if this properly updated dependant and has the proper principal relationship.
+ /// TODO check if this properly updated Dependant and has the proper principal relationship.
/// </remarks>
public virtual CollectionItem Next { get; set; }
@@ -81,7 +81,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Gets or sets the previous item in the collection.
/// </summary>
/// <remarks>
- /// TODO check if this properly updated dependant and has the proper principal relationship.
+ /// TODO check if this properly updated Dependant and has the proper principal relationship.
/// </remarks>
public virtual CollectionItem Previous { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
index 1d2dc0f66..d74330c05 100644
--- a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
@@ -141,7 +141,7 @@ namespace Jellyfin.Data.Entities.Libraries
public virtual ICollection<PersonRole> PersonRoles { get; protected set; }
/// <summary>
- /// Gets or sets a collection containing the generes for this item.
+ /// Gets or sets a collection containing the genres for this item.
/// </summary>
public virtual ICollection<Genre> Genres { get; protected set; }
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 5038988f9..9ae129d07 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -41,8 +41,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.9" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.9" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index c11ac5fb3..466a12e67 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 6a9dbdae4..ee60748c7 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using BlurHashSharp.SkiaSharp;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Drawing;
@@ -227,8 +228,8 @@ namespace Jellyfin.Drawing.Skia
}
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
-
- Directory.CreateDirectory(Path.GetDirectoryName(tempPath));
+ var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
+ Directory.CreateDirectory(directory);
File.Copy(path, tempPath, true);
return tempPath;
@@ -493,7 +494,8 @@ namespace Jellyfin.Drawing.Skia
// If all we're doing is resizing then we can stop now
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
{
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ Directory.CreateDirectory(outputDirectory);
using var outputStream = new SKFileWStream(outputPath);
using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
@@ -540,7 +542,8 @@ namespace Jellyfin.Drawing.Skia
DrawIndicator(canvas, width, height, options);
}
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ Directory.CreateDirectory(directory);
using (var outputStream = new SKFileWStream(outputPath))
{
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index c52be3b8a..e663798da 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -24,12 +24,12 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="System.Linq.Async" Version="4.1.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.9">
+ <PackageReference Include="System.Linq.Async" Version="5.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.9">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
index f79e433a6..662b4bf65 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.Server.Implementations.Users
bool success = false;
- // As long as jellyfin supports passwordless users, we need this little block here to accommodate
+ // As long as jellyfin supports password-less users, we need this little block here to accommodate
if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
{
return Task.FromResult(new ProviderAuthenticationResult
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index 6cb13cd23..334f27f85 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -57,7 +57,8 @@ namespace Jellyfin.Server.Implementations.Users
SerializablePasswordReset spr;
await using (var str = File.OpenRead(resetFile))
{
- spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
+ spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
+ ?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
}
if (spr.ExpirationDate < DateTime.UtcNow)
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 40b89ed28..f684d151d 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -158,7 +158,6 @@ namespace Jellyfin.Server.Implementations.Users
user.Username = newName;
await UpdateUserAsync(user).ConfigureAwait(false);
-
OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
}
@@ -167,6 +166,7 @@ namespace Jellyfin.Server.Implementations.Users
{
using var dbContext = _dbProvider.CreateContext();
dbContext.Users.Update(user);
+ _users[user.Id] = user;
dbContext.SaveChanges();
}
@@ -175,7 +175,7 @@ namespace Jellyfin.Server.Implementations.Users
{
await using var dbContext = _dbProvider.CreateContext();
dbContext.Users.Update(user);
-
+ _users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
@@ -642,6 +642,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user);
+ _users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
@@ -713,6 +714,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);
+ _users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
@@ -723,6 +725,7 @@ namespace Jellyfin.Server.Implementations.Users
dbContext.Remove(user.ProfileImage);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
user.ProfileImage = null;
+ _users[user.Id] = user;
}
private static bool IsValidUsername(string name)
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index c44736447..cb8ae91f5 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -13,6 +13,7 @@ using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
@@ -76,6 +77,7 @@ namespace Jellyfin.Server
options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
ServiceCollection.AddEventServices();
+ ServiceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
ServiceCollection.AddSingleton<IEventManager, EventManager>();
ServiceCollection.AddSingleton<JellyfinDbProvider>();
diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs
index 8ea35c281..7ad9466c1 100644
--- a/Jellyfin.Server/Filters/FileResponseFilter.cs
+++ b/Jellyfin.Server/Filters/FileResponseFilter.cs
@@ -26,22 +26,22 @@ namespace Jellyfin.Server.Filters
if (attribute is ProducesFileAttribute producesFileAttribute)
{
// Get operation response values.
- var (_, value) = operation.Responses
+ var response = operation.Responses
.FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
// Operation doesn't have a response.
- if (value == null)
+ if (response.Value == null)
{
continue;
}
// Clear existing responses.
- value.Content.Clear();
+ response.Value.Content.Clear();
// Add all content-types as file.
foreach (var contentType in producesFileAttribute.GetContentTypes())
{
- value.Content.Add(contentType, _openApiMediaType);
+ response.Value.Content.Add(contentType, _openApiMediaType);
}
break;
diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
index b3771b7fe..e8dd48e4e 100644
--- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs
+++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
@@ -30,7 +30,8 @@ namespace Jellyfin.Server.Formatters
/// <returns>Write stream task.</returns>
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
- return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
+ var stringResponse = context.Object?.ToString();
+ return stringResponse == null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
}
}
}
diff --git a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs b/Jellyfin.Server/Formatters/XmlOutputFormatter.cs
index 01d99d7c8..be0baea2d 100644
--- a/Jellyfin.Server/Formatters/XmlOutputFormatter.cs
+++ b/Jellyfin.Server/Formatters/XmlOutputFormatter.cs
@@ -26,7 +26,8 @@ namespace Jellyfin.Server.Formatters
/// <inheritdoc />
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
- return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
+ var stringResponse = context.Object?.ToString();
+ return stringResponse == null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
}
}
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 3558f144c..03d06fdff 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -8,7 +8,7 @@
<PropertyGroup>
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -38,10 +38,10 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
index 06a29a0db..a259cb7bc 100644
--- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
@@ -43,7 +43,8 @@ namespace MediaBrowser.Common.Json.Converters
}
catch (FormatException)
{
- // TODO log when upgraded to .Net5
+ // TODO log when upgraded to .Net6
+ // https://github.com/dotnet/runtime/issues/42975
// _logger.LogWarning(e, "Error converting value.");
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonDateTimeIso8601Converter.cs b/MediaBrowser.Common/Json/Converters/JsonDateTimeIso8601Converter.cs
new file mode 100644
index 000000000..63b344a9d
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonDateTimeIso8601Converter.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Returns an ISO8601 formatted datetime.
+ /// </summary>
+ /// <remarks>
+ /// Used for legacy compatibility.
+ /// </remarks>
+ public class JsonDateTimeIso8601Converter : JsonConverter<DateTime>
+ {
+ /// <inheritdoc />
+ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => reader.GetDateTime();
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture));
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
new file mode 100644
index 000000000..75fbcea1f
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
@@ -0,0 +1,75 @@
+using System;
+using System.ComponentModel;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Convert Pipe delimited string to array of type.
+ /// </summary>
+ /// <typeparam name="T">Type to convert to.</typeparam>
+ public class JsonPipeDelimitedArrayConverter<T> : JsonConverter<T[]>
+ {
+ private readonly TypeConverter _typeConverter;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class.
+ /// </summary>
+ public JsonPipeDelimitedArrayConverter()
+ {
+ _typeConverter = TypeDescriptor.GetConverter(typeof(T));
+ }
+
+ /// <inheritdoc />
+ public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var stringEntries = reader.GetString()?.Split('|', StringSplitOptions.RemoveEmptyEntries);
+ if (stringEntries == null || stringEntries.Length == 0)
+ {
+ return Array.Empty<T>();
+ }
+
+ var parsedValues = new object[stringEntries.Length];
+ var convertedCount = 0;
+ for (var i = 0; i < stringEntries.Length; i++)
+ {
+ try
+ {
+ parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
+ convertedCount++;
+ }
+ catch (FormatException)
+ {
+ // TODO log when upgraded to .Net6
+ // https://github.com/dotnet/runtime/issues/42975
+ // _logger.LogWarning(e, "Error converting value.");
+ }
+ }
+
+ var typedValues = new T[convertedCount];
+ var typedValueIndex = 0;
+ for (var i = 0; i < stringEntries.Length; i++)
+ {
+ if (parsedValues[i] != null)
+ {
+ typedValues.SetValue(parsedValues[i], typedValueIndex);
+ typedValueIndex++;
+ }
+ }
+
+ return typedValues;
+ }
+
+ return JsonSerializer.Deserialize<T[]>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value, options);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
new file mode 100644
index 000000000..5e77223ef
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Json Pipe delimited array converter factory.
+ /// </summary>
+ /// <remarks>
+ /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
+ /// </remarks>
+ public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory
+ {
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return true;
+ }
+
+ /// <inheritdoc />
+ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
+ return (JsonConverter)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType));
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 6605ae962..9a94664ac 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -42,6 +42,7 @@ namespace MediaBrowser.Common.Json
options.Converters.Add(new JsonGuidConverter());
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new JsonNullableStructConverterFactory());
+ options.Converters.Add(new JsonDateTimeIso8601Converter());
return options;
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index e716a6610..be5e7f5b4 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
@@ -18,9 +18,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.9" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>
@@ -29,7 +29,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs b/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs
index e189d6e70..f1c5f2477 100644
--- a/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs
+++ b/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs
@@ -13,8 +13,7 @@ namespace MediaBrowser.Common.Net
/// </summary>
public DefaultHttpClientHandler()
{
- // TODO change to DecompressionMethods.All with .NET5
- AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
+ AutomaticDecompression = DecompressionMethods.All;
}
}
}
diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs
index a0330afef..12966a474 100644
--- a/MediaBrowser.Common/Net/INetworkManager.cs
+++ b/MediaBrowser.Common/Net/INetworkManager.cs
@@ -58,7 +58,7 @@ namespace MediaBrowser.Common.Net
/// <summary>
/// Investigates an caches a list of interface addresses, excluding local link and LAN excluded addresses.
/// </summary>
- /// <returns>The list of ipaddresses.</returns>
+ /// <returns>The list of ip addresses.</returns>
IPAddress[] GetLocalIpAddresses();
/// <summary>
@@ -73,7 +73,7 @@ namespace MediaBrowser.Common.Net
/// Returns true if address is in the LAN list in the config file.
/// </summary>
/// <param name="address">The address to check.</param>
- /// <param name="excludeInterfaces">If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address.</param>
+ /// <param name="excludeInterfaces">If true, check against addresses in the LAN settings which have [] around and return true if it matches the address give in address.</param>
/// <param name="excludeRFC">If true, returns false if address is in the 127.x.x.x or 169.128.x.x range.</param>
/// <returns><c>false</c>if the address isn't in the LAN list, <c>true</c> if the address has been defined as a LAN address.</returns>
bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC);
diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs
index e21d8c7d1..084e91d50 100644
--- a/MediaBrowser.Common/Plugins/BasePlugin.cs
+++ b/MediaBrowser.Common/Plugins/BasePlugin.cs
@@ -247,23 +247,34 @@ namespace MediaBrowser.Common.Plugins
}
catch
{
- return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
+ var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
+ SaveConfiguration(config);
+ return config;
}
}
/// <summary>
/// Saves the current configuration to the file system.
/// </summary>
- public virtual void SaveConfiguration()
+ /// <param name="config">Configuration to save.</param>
+ public virtual void SaveConfiguration(TConfigurationType config)
{
lock (_configurationSaveLock)
{
_directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
- XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
+ XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
}
}
+ /// <summary>
+ /// Saves the current configuration to the file system.
+ /// </summary>
+ public virtual void SaveConfiguration()
+ {
+ SaveConfiguration(Configuration);
+ }
+
/// <inheritdoc />
public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
{
@@ -274,9 +285,9 @@ namespace MediaBrowser.Common.Plugins
Configuration = (TConfigurationType)configuration;
- SaveConfiguration();
+ SaveConfiguration(Configuration);
- ConfigurationChanged.Invoke(this, configuration);
+ ConfigurationChanged?.Invoke(this, configuration);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
new file mode 100644
index 000000000..67aa7f338
--- /dev/null
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Linq;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.BaseItemManager
+{
+ /// <inheritdoc />
+ public class BaseItemManager : IBaseItemManager
+ {
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseItemManager"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public BaseItemManager(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc />
+ public bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name)
+ {
+ if (baseItem is Channel)
+ {
+ // Hack alert.
+ return true;
+ }
+
+ if (baseItem.SourceType == SourceType.Channel)
+ {
+ // Hack alert.
+ return !baseItem.EnableMediaSourceDisplay;
+ }
+
+ var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
+ if (typeOptions != null)
+ {
+ return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (!libraryOptions.EnableInternetProviders)
+ {
+ return false;
+ }
+
+ var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+
+ return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <inheritdoc />
+ public bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name)
+ {
+ if (baseItem is Channel)
+ {
+ // Hack alert.
+ return true;
+ }
+
+ if (baseItem.SourceType == SourceType.Channel)
+ {
+ // Hack alert.
+ return !baseItem.EnableMediaSourceDisplay;
+ }
+
+ var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
+ if (typeOptions != null)
+ {
+ return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (!libraryOptions.EnableInternetProviders)
+ {
+ return false;
+ }
+
+ var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+
+ return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
new file mode 100644
index 000000000..ee4d3dcdc
--- /dev/null
+++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.BaseItemManager
+{
+ /// <summary>
+ /// The <c>BaseItem</c> manager.
+ /// </summary>
+ public interface IBaseItemManager
+ {
+ /// <summary>
+ /// Is metadata fetcher enabled.
+ /// </summary>
+ /// <param name="baseItem">The base item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="name">The metadata fetcher name.</param>
+ /// <returns><c>true</c> if metadata fetcher is enabled, else false.</returns>
+ bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name);
+
+ /// <summary>
+ /// Is image fetcher enabled.
+ /// </summary>
+ /// <param name="baseItem">The base item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="name">The image fetcher name.</param>
+ /// <returns><c>true</c> if image fetcher is enabled, else false.</returns>
+ bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 1d44a5511..1b25fbdbb 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -463,60 +463,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
- public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name)
- {
- if (SourceType == SourceType.Channel)
- {
- // hack alert
- return !EnableMediaSourceDisplay;
- }
-
- var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
- if (typeOptions != null)
- {
- return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
- }
-
- if (!libraryOptions.EnableInternetProviders)
- {
- return false;
- }
-
- var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
-
- return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
- }
-
- public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name)
- {
- if (this is Channel)
- {
- // hack alert
- return true;
- }
-
- if (SourceType == SourceType.Channel)
- {
- // hack alert
- return !EnableMediaSourceDisplay;
- }
-
- var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
- if (typeOptions != null)
- {
- return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
- }
-
- if (!libraryOptions.EnableInternetProviders)
- {
- return false;
- }
-
- var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
-
- return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
- }
-
public virtual bool CanDelete()
{
if (SourceType == SourceType.Channel)
@@ -2611,7 +2557,7 @@ namespace MediaBrowser.Controller.Entities
{
if (!AllowsMultipleImages(type))
{
- throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots");
+ throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots");
}
var info1 = GetImageInfo(type, index1);
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index a76c8a376..3087b1d1e 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -212,7 +212,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Loads our children. Validation will occur externally.
- /// We want this sychronous.
+ /// We want this synchronous.
/// </summary>
protected virtual List<BaseItem> LoadChildren()
{
@@ -1067,12 +1067,12 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.Genres.Length > 0)
+ if (request.Genres.Count > 0)
{
return false;
}
- if (request.GenreIds.Length > 0)
+ if (request.GenreIds.Count > 0)
{
return false;
}
@@ -1177,7 +1177,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.GenreIds.Length > 0)
+ if (request.GenreIds.Count > 0)
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 904752a22..270217356 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.Entities
public string[] ExcludeInheritedTags { get; set; }
- public string[] Genres { get; set; }
+ public IReadOnlyList<string> Genres { get; set; }
public bool? IsSpecialSeason { get; set; }
@@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.Entities
public Guid[] StudioIds { get; set; }
- public Guid[] GenreIds { get; set; }
+ public IReadOnlyList<Guid> GenreIds { get; set; }
public ImageType[] ImageTypes { get; set; }
@@ -162,7 +162,7 @@ namespace MediaBrowser.Controller.Entities
public double? MinCommunityRating { get; set; }
- public Guid[] ChannelIds { get; set; }
+ public IReadOnlyList<Guid> ChannelIds { get; set; }
public int? ParentIndexNumber { get; set; }
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index a262fee15..4e33a6bbd 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -791,7 +791,7 @@ namespace MediaBrowser.Controller.Entities
}
// Apply genre filter
- if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
+ if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
{
return false;
}
@@ -822,7 +822,7 @@ namespace MediaBrowser.Controller.Entities
}
// Apply genre filter
- if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id =>
+ if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id =>
{
var genreItem = libraryManager.GetItemById(id);
return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase);
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 4374317d6..9acc98dce 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.9" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>
@@ -29,7 +29,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 5846a603a..9a6f1231f 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
@@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
public class EncodingHelper
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
@@ -63,7 +64,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
- // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+ // Since transcoding of folder rips is experimental anyway, it's not worth adding additional variables such as this.
if (state.VideoType == VideoType.VideoFile)
{
var hwType = encodingOptions.HardwareAccelerationType;
@@ -247,7 +248,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
- // Seeing reported failures here, not sure yet if this is related to specfying input format
+ // Seeing reported failures here, not sure yet if this is related to specifying input format
if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase))
{
return null;
@@ -440,6 +441,12 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libopus";
}
+ if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
+ {
+ // flac is experimental in mp4 muxer
+ return "flac -strict -2";
+ }
+
return codec.ToLowerInvariant();
}
@@ -573,7 +580,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
- public bool IsH264(MediaStream stream)
+ public static bool IsH264(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
@@ -581,7 +588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
}
- public bool IsH265(MediaStream stream)
+ public static bool IsH265(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
@@ -589,10 +596,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
}
- // TODO This is auto inserted into the mpegts mux so it might not be needed
- // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
- public string GetBitStreamArgs(MediaStream stream)
+ public static bool IsAAC(MediaStream stream)
+ {
+ var codec = stream.Codec ?? string.Empty;
+
+ return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ public static string GetBitStreamArgs(MediaStream stream)
{
+ // TODO This is auto inserted into the mpegts mux so it might not be needed.
+ // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
if (IsH264(stream))
{
return "-bsf:v h264_mp4toannexb";
@@ -601,12 +615,44 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return "-bsf:v hevc_mp4toannexb";
}
+ else if (IsAAC(stream))
+ {
+ // Convert adts header(mpegts) to asc header(mp4).
+ return "-bsf:a aac_adtstoasc";
+ }
else
{
return null;
}
}
+ public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
+ {
+ var bitStreamArgs = string.Empty;
+ var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
+
+ // Apply aac_adtstoasc bitstream filter when media source is in mpegts.
+ if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
+ {
+ bitStreamArgs = GetBitStreamArgs(state.AudioStream);
+ bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
+ }
+
+ return bitStreamArgs;
+ }
+
+ public static string GetSegmentFileExtension(string segmentContainer)
+ {
+ if (!string.IsNullOrWhiteSpace(segmentContainer))
+ {
+ return "." + segmentContainer;
+ }
+
+ return ".ts";
+ }
+
public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
var bitrate = state.OutputVideoBitrate;
@@ -654,16 +700,30 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- public string NormalizeTranscodingLevel(string videoCodec, string level)
+ public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
- // Clients may direct play higher than level 41, but there's no reason to transcode higher
- if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel)
- && requestLevel > 41
- && (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)))
+ if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel))
{
- return "41";
+ if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
+ {
+ // Transcode to level 5.0 and lower for maximum compatibility.
+ // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
+ // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
+ // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
+ if (requestLevel >= 150)
+ {
+ return "150";
+ }
+ }
+ else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ // Clients may direct play higher than level 41, but there's no reason to transcode higher.
+ if (requestLevel >= 41)
+ {
+ return "41";
+ }
+ }
}
return level;
@@ -766,6 +826,72 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
+ public string GetHlsVideoKeyFrameArguments(
+ EncodingJobInfo state,
+ string codec,
+ int segmentLength,
+ bool isEventPlaylist,
+ int? startNumber)
+ {
+ var args = string.Empty;
+ var gopArg = string.Empty;
+ var keyFrameArg = string.Empty;
+ if (isEventPlaylist)
+ {
+ keyFrameArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"",
+ segmentLength);
+ }
+ else if (startNumber.HasValue)
+ {
+ keyFrameArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
+ startNumber.Value * segmentLength,
+ segmentLength);
+ }
+
+ var framerate = state.VideoStream?.RealFrameRate;
+ if (framerate.HasValue)
+ {
+ // This is to make sure keyframe interval is limited to our segment,
+ // as forcing keyframes is not enough.
+ // Example: we encoded half of desired length, then codec detected
+ // scene cut and inserted a keyframe; next forced keyframe would
+ // be created outside of segment, which breaks seeking.
+ // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe.
+ gopArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -g:v:0 {0} -keyint_min:v:0 {0} -sc_threshold:v:0 0",
+ Math.Ceiling(segmentLength * framerate.Value));
+ }
+
+ // Unable to force key frames using these encoders, set key frames by GOP.
+ if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ args += gopArg;
+ }
+ else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ args += " " + keyFrameArg;
+ }
+ else
+ {
+ args += " " + keyFrameArg + gopArg;
+ }
+
+ return args;
+ }
+
/// <summary>
/// Gets the video bitrate to specify on the command line.
/// </summary>
@@ -773,6 +899,47 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var param = string.Empty;
+ if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -pix_fmt yuv420p";
+ }
+
+ if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ var videoStream = state.VideoStream;
+ var isColorDepth10 = IsColorDepth10(state);
+
+ if (isColorDepth10
+ && _mediaEncoder.SupportsHwaccel("opencl")
+ && encodingOptions.EnableTonemapping
+ && !string.IsNullOrEmpty(videoStream.VideoRange)
+ && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -pix_fmt nv12";
+ }
+ else
+ {
+ param += " -pix_fmt yuv420p";
+ }
+ }
+
+ if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -pix_fmt nv21";
+ }
+
var isVc1 = state.VideoStream != null &&
string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
@@ -781,11 +948,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset))
{
- param += "-preset " + encodingOptions.EncoderPreset;
+ param += " -preset " + encodingOptions.EncoderPreset;
}
else
{
- param += "-preset " + defaultPreset;
+ param += " -preset " + defaultPreset;
}
int encodeCrf = encodingOptions.H264Crf;
@@ -809,38 +976,40 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -crf " + defaultCrf;
}
}
- else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) // h264 (h264_qsv)
+ else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
{
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase))
{
- param += "-preset " + encodingOptions.EncoderPreset;
+ param += " -preset " + encodingOptions.EncoderPreset;
}
else
{
- param += "-preset 7";
+ param += " -preset 7";
}
param += " -look_ahead 0";
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
- || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
{
+ // following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead.
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
- param += "-preset slow"; // lossless is only supported on maxwell and newer(2014+)
+ param += " -preset slow"; // lossless is only supported on maxwell and newer(2014+)
break;
case "slow":
case "slower":
- param += "-preset slow";
+ param += " -preset slow";
break;
case "medium":
- param += "-preset medium";
+ param += " -preset medium";
break;
case "fast":
@@ -848,27 +1017,27 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast":
case "superfast":
case "ultrafast":
- param += "-preset fast";
+ param += " -preset fast";
break;
default:
- param += "-preset default";
+ param += " -preset default";
break;
}
}
- else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
{
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
case "slow":
case "slower":
- param += "-quality quality";
+ param += " -quality quality";
break;
case "medium":
- param += "-quality balanced";
+ param += " -quality balanced";
break;
case "fast":
@@ -876,11 +1045,11 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast":
case "superfast":
case "ultrafast":
- param += "-quality speed";
+ param += " -quality speed";
break;
default:
- param += "-quality speed";
+ param += " -quality speed";
break;
}
@@ -896,6 +1065,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// Enhance workload when tone mapping with AMF on some APUs
param += " -preanalysis true";
}
+
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -header_insertion_mode gop -gops_per_idr 1";
+ }
}
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{
@@ -917,7 +1091,7 @@ namespace MediaBrowser.Controller.MediaEncoding
profileScore = Math.Min(profileScore, 2);
// http://www.webmproject.org/docs/encoder-parameters/
- param += string.Format(CultureInfo.InvariantCulture, "-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
+ param += string.Format(CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
profileScore.ToString(_usCulture),
crf,
qmin,
@@ -925,15 +1099,15 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
{
- param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
+ param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
}
else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv
{
- param += "-qmin 2";
+ param += " -qmin 2";
}
else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
{
- param += "-mbd 2";
+ param += " -mbd 2";
}
param += GetVideoBitrateParam(state, videoEncoder);
@@ -945,11 +1119,25 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var targetVideoCodec = state.ActualOutputVideoCodec;
+ if (string.Equals(targetVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(targetVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ targetVideoCodec = "hevc";
+ }
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault();
+ profile = Regex.Replace(profile, @"\s+", String.Empty);
- // vaapi does not support Baseline profile, force Constrained Baseline in this case,
- // which is compatible (and ugly)
+ // Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
+ if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
+ && profile != null
+ && profile.IndexOf("high 10", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ profile = "high";
+ }
+
+ // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
+ // which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& profile != null
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
@@ -957,13 +1145,31 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "constrained_baseline";
}
+ // libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case.
+ if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ && profile != null
+ && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ profile = "baseline";
+ }
+
+ // Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile.
+ if (!string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ && profile != null
+ && profile.IndexOf("main 10", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ profile = "main";
+ }
+
if (!string.IsNullOrEmpty(profile))
{
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
// not supported by h264_omx
- param += " -profile:v " + profile;
+ param += " -profile:v:0 " + profile;
}
}
@@ -971,55 +1177,35 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(level))
{
- level = NormalizeTranscodingLevel(state.OutputVideoCodec, level);
+ level = NormalizeTranscodingLevel(state, level);
- // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
- // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
+ // libx264, QSV, AMF, VAAPI can adjust the given level to match the output.
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -level " + level;
+ }
+ else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
{
- switch (level)
+ // hevc_qsv use -level 51 instead of -level 153.
+ if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
{
- case "30":
- param += " -level 3.0";
- break;
- case "31":
- param += " -level 3.1";
- break;
- case "32":
- param += " -level 3.2";
- break;
- case "40":
- param += " -level 4.0";
- break;
- case "41":
- param += " -level 4.1";
- break;
- case "42":
- param += " -level 4.2";
- break;
- case "50":
- param += " -level 5.0";
- break;
- case "51":
- param += " -level 5.1";
- break;
- case "52":
- param += " -level 5.2";
- break;
- default:
- param += " -level " + level;
- break;
+ param += " -level " + hevcLevel / 3;
}
}
+ else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -level " + level;
+ }
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
{
- // nvenc doesn't decode with param -level set ?!
- // TODO:
+ // level option may cause NVENC to fail.
+ // NVENC cannot adjust the given level, just throw an error.
}
- else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
+ || !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
param += " -level " + level;
}
@@ -1032,42 +1218,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
- // todo
- }
-
- if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
- {
- param = "-pix_fmt yuv420p " + param;
- }
-
- if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase))
- {
- var videoStream = state.VideoStream;
- var isColorDepth10 = IsColorDepth10(state);
-
- if (isColorDepth10
- && _mediaEncoder.SupportsHwaccel("opencl")
- && encodingOptions.EnableTonemapping
- && !string.IsNullOrEmpty(videoStream.VideoRange)
- && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
- {
- param = "-pix_fmt nv12 " + param;
- }
- else
- {
- param = "-pix_fmt yuv420p " + param;
- }
- }
-
- if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
- {
- param = "-pix_fmt nv21 " + param;
+ // libx265 only accept level option in -x265-params.
+ // level option may cause libx265 to fail.
+ // libx265 cannot adjust the given level, just throw an error.
+ // TODO: set fine tuned params.
+ param += " -x265-params:0 no-info=1";
}
return param;
@@ -1346,7 +1501,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
- return .5;
+ return .6;
}
return 1;
@@ -1380,36 +1535,48 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
{
- if (audioStream == null)
- {
- return null;
- }
-
- if (request.AudioBitRate.HasValue)
- {
- // Don't encode any higher than this
- return Math.Min(384000, request.AudioBitRate.Value);
- }
-
- // Empty bitrate area is not allow on iOS
- // Default audio bitrate to 128K if it is not being requested
- // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
- return 128000;
+ return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
}
- public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
+ public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
{
if (audioStream == null)
{
return null;
}
- if (audioBitRate.HasValue)
+ if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
{
- // Don't encode any higher than this
return Math.Min(384000, audioBitRate.Value);
}
+ if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
+ {
+ if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ if ((audioStream.Channels ?? 0) >= 6)
+ {
+ return Math.Min(640000, audioBitRate.Value);
+ }
+
+ return Math.Min(384000, audioBitRate.Value);
+ }
+
+ if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ if ((audioStream.Channels ?? 0) >= 6)
+ {
+ return Math.Min(3584000, audioBitRate.Value);
+ }
+
+ return Math.Min(1536000, audioBitRate.Value);
+ }
+ }
+
// Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K if it is not being requested
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
@@ -1447,7 +1614,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (filters.Count > 0)
{
- return "-af \"" + string.Join(",", filters) + "\"";
+ return " -af \"" + string.Join(",", filters) + "\"";
}
return string.Empty;
@@ -1462,6 +1629,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.Nullable{System.Int32}.</returns>
public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec)
{
+ if (audioStream == null)
+ {
+ return null;
+ }
+
var request = state.BaseRequest;
var inputChannels = audioStream?.Channels;
@@ -1484,6 +1656,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// libmp3lame currently only supports two channel output
transcoderChannelLimit = 2;
}
+ else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ // aac is able to handle 8ch(7.1 layout)
+ transcoderChannelLimit = 8;
+ }
else
{
// If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
@@ -1708,7 +1885,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// For QSV, feed it into hardware encoder now
- if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)))
{
videoSizeParam += ",hwupload=extra_hw_frames=64";
}
@@ -1729,7 +1907,8 @@ namespace MediaBrowser.Controller.MediaEncoding
: " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
{
/*
[base]: HW scaling video to OutputSize
@@ -1741,7 +1920,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
- && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+ && (string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "libx265", StringComparison.OrdinalIgnoreCase)))
{
/*
[base]: SW scaling video to OutputSize
@@ -1750,7 +1930,8 @@ namespace MediaBrowser.Controller.MediaEncoding
*/
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
}
- else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
{
/*
QSV in FFMpeg can now setup hardware overlay for transcodes.
@@ -1776,7 +1957,7 @@ namespace MediaBrowser.Controller.MediaEncoding
videoSizeParam);
}
- private (int? width, int? height) GetFixedOutputSize(
+ public static (int? width, int? height) GetFixedOutputSize(
int? videoWidth,
int? videoHeight,
int? requestedWidth,
@@ -1836,7 +2017,9 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxHeight);
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
&& width.HasValue
&& height.HasValue)
{
@@ -1845,7 +2028,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// output dimensions. Output dimensions are guaranteed to be even.
var outputWidth = width.Value;
var outputHeight = height.Value;
- var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
+ var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase);
var isDeintEnabled = state.DeInterlace("h264", true)
|| state.DeInterlace("avc", true)
|| state.DeInterlace("h265", true)
@@ -2107,10 +2291,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
+ var isQsvHevcEncoder = outputVideoCodec.IndexOf("hevc_qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
+ var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1;
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isColorDepth10 = IsColorDepth10(state);
@@ -2185,6 +2372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add("hwdownload");
if (isLibX264Encoder
+ || isLibX265Encoder
|| hasGraphicalSubs
|| (isNvdecHevcDecoder && isDeinterlaceHevc)
|| (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
@@ -2195,20 +2383,20 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// When the input may or may not be hardware VAAPI decodable
- if (isVaapiH264Encoder)
+ if (isVaapiH264Encoder || isVaapiHevcEncoder)
{
filters.Add("format=nv12|vaapi");
filters.Add("hwupload");
}
// When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
- else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
+ else if (isLinux && hasGraphicalSubs && (isQsvH264Encoder || isQsvHevcEncoder))
{
filters.Add("hwupload=extra_hw_frames=64");
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
- else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
+ else if (IsVaapiSupported(state) && isVaapiDecoder && (isLibX264Encoder || isLibX265Encoder))
{
var codec = videoStream.Codec.ToLowerInvariant();
@@ -2250,7 +2438,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Add software deinterlace filter before scaling filter
if ((isDeinterlaceH264 || isDeinterlaceHevc)
&& !isVaapiH264Encoder
+ && !isVaapiHevcEncoder
&& !isQsvH264Encoder
+ && !isQsvHevcEncoder
&& !isNvdecH264Decoder)
{
if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase))
@@ -2289,7 +2479,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
- if (isVaapiH264Encoder)
+ if (isVaapiH264Encoder || isVaapiHevcEncoder)
{
if (hasTextSubs)
{
@@ -2329,7 +2519,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the number of threads.
/// </summary>
- public int GetNumberOfThreads(EncodingJobInfo state, EncodingOptions encodingOptions, string outputVideoCodec)
+#nullable enable
+ public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec)
{
if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
{
@@ -2339,17 +2530,21 @@ namespace MediaBrowser.Controller.MediaEncoding
return Math.Max(Environment.ProcessorCount - 1, 1);
}
- var threads = state.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
+ var threads = state?.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
// Automatic
- if (threads <= 0 || threads >= Environment.ProcessorCount)
+ if (threads <= 0)
{
return 0;
+ }
+ else if (threads >= Environment.ProcessorCount)
+ {
+ return Environment.ProcessorCount;
}
return threads;
}
-
+#nullable disable
public void TryStreamCopy(EncodingJobInfo state)
{
if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream))
@@ -2557,6 +2752,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public void AttachMediaSourceInfo(
EncodingJobInfo state,
+ EncodingOptions encodingOptions,
MediaSourceInfo mediaSource,
string requestedUrl)
{
@@ -2687,11 +2883,23 @@ namespace MediaBrowser.Controller.MediaEncoding
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
?? state.SupportedAudioCodecs.FirstOrDefault();
}
+
+ var supportedVideoCodecs = state.SupportedVideoCodecs;
+ if (request != null && supportedVideoCodecs != null && supportedVideoCodecs.Length > 0)
+ {
+ var supportedVideoCodecsList = supportedVideoCodecs.ToList();
+
+ ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions);
+
+ state.SupportedVideoCodecs = supportedVideoCodecsList.ToArray();
+
+ request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+ }
}
private void ShiftAudioCodecsIfNeeded(List<string> audioCodecs, MediaStream audioStream)
{
- // Nothing to do here
+ // No need to shift if there is only one supported audio codec.
if (audioCodecs.Count < 2)
{
return;
@@ -2719,6 +2927,34 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions)
+ {
+ // Shift hevc/h265 to the end of list if hevc encoding is not allowed.
+ if (encodingOptions.AllowHevcEncoding)
+ {
+ return;
+ }
+
+ // No need to shift if there is only one supported video codec.
+ if (videoCodecs.Count < 2)
+ {
+ return;
+ }
+
+ var shiftVideoCodecs = new[] { "hevc", "h265" };
+ if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ {
+ return;
+ }
+
+ while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparer.OrdinalIgnoreCase))
+ {
+ var removed = shiftVideoCodecs[0];
+ videoCodecs.RemoveAt(0);
+ videoCodecs.Add(removed);
+ }
+ }
+
private void NormalizeSubtitleEmbed(EncodingJobInfo state)
{
if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
@@ -2752,7 +2988,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
- // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+ // Since transcoding of folder rips is experimental anyway, it's not worth adding additional variables such as this.
if (videoType != VideoType.VideoFile)
{
return null;
@@ -3352,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
}
- args += " " + GetAudioFilterParam(state, encodingOptions, false);
+ args += GetAudioFilterParam(state, encodingOptions, false);
return args;
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 6e9362cd1..52794a69b 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -409,7 +409,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Don't exceed what the encoder supports
// Seeing issues of attempting to encode to 88200
- return Math.Min(44100, BaseRequest.AudioSampleRate.Value);
+ return BaseRequest.AudioSampleRate.Value;
}
return null;
@@ -593,6 +593,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
get
{
+ if (VideoStream == null)
+ {
+ return null;
+ }
+
if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
{
return VideoStream?.Codec;
@@ -606,6 +611,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
get
{
+ if (AudioStream == null)
+ {
+ return null;
+ }
+
if (EncodingHelper.IsCopyCodec(OutputAudioCodec))
{
return AudioStream?.Codec;
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index fbf2c5213..f6c592070 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="itemIds">The item ids.</param>
/// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns>
- Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId);
+ Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary>
/// Removes from playlist.
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index 914db5305..84c3ed8b0 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -486,7 +486,7 @@ namespace MediaBrowser.LocalMetadata.Images
return false;
}
- private FileSystemMetadata GetImage(IEnumerable<FileSystemMetadata> files, string name)
+ private FileSystemMetadata? GetImage(IEnumerable<FileSystemMetadata> files, string name)
{
return files.FirstOrDefault(i => !i.IsDirectory && string.Equals(name, _fileSystem.GetFileNameWithoutExtension(i), StringComparison.OrdinalIgnoreCase) && i.Length > 0);
}
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 529e7065c..3ce9ff4cc 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -11,7 +11,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index 4ac249840..5d3ab30d3 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -683,7 +683,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
default:
{
string readerName = reader.Name;
- if (_validProviderIds!.TryGetValue(readerName, out string providerIdValue))
+ if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
{
var id = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(id))
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index 7a4823e1b..396206658 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -127,7 +128,8 @@ namespace MediaBrowser.LocalMetadata.Savers
private void SaveToFile(Stream stream, string path)
{
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
+ Directory.CreateDirectory(directory);
// On Windows, savint the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 21b5d0c5b..bc940d0b8 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -178,7 +178,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
process.Start();
- var ranToCompletion = await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
if (!ranToCompletion)
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 3287f9814..92f16ab95 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -25,6 +25,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"ac3",
"aac",
"mp3",
+ "flac",
"h264_qsv",
"hevc_qsv",
"mpeg2_qsv",
@@ -71,6 +72,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"libmp3lame",
"libopus",
"libvorbis",
+ "flac",
"srt",
"h264_amf",
"hevc_amf",
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 5a3a9185d..5f60c09ae 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -64,6 +64,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private string _ffmpegPath = string.Empty;
private string _ffprobePath;
+ private int threads;
public MediaEncoder(
ILogger<MediaEncoder> logger,
@@ -129,6 +130,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableDecoders(validator.GetDecoders());
SetAvailableEncoders(validator.GetEncoders());
SetAvailableHwaccels(validator.GetHwaccels());
+ threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
}
_logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty);
@@ -377,9 +379,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
CancellationToken cancellationToken)
{
var args = extractChapters
- ? "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_chapters -show_format"
- : "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_format";
- args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath).Trim();
+ ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
+ : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
+ args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, threads).Trim();
var process = new Process
{
@@ -520,29 +522,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
- // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600.
+ // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar.
// This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar
- var vf = "scale=600:trunc(600/dar/2)*2";
+ var vf = string.Empty;
if (threedFormat.HasValue)
{
switch (threedFormat.Value)
{
case Video3DFormat.HalfSideBySide:
- vf = "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2";
- // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to 600. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
+ vf = "-vf crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
+ // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
break;
case Video3DFormat.FullSideBySide:
- vf = "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2";
- // fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to 600.
+ vf = "-vf crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
+ // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
break;
case Video3DFormat.HalfTopAndBottom:
- vf = "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2";
- // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to 600
+ vf = "-vf crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
+ // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made
break;
case Video3DFormat.FullTopAndBottom:
- vf = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2";
- // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made the scale width to 600
+ vf = "-vf crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
+ // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made
break;
default:
break;
@@ -555,8 +557,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
var thumbnail = enableThumbnail ? ",thumbnail=24" : string.Empty;
- var args = useIFrame ? string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}{4}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail) :
- string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg);
+ var args = useIFrame ? string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {5} -v quiet -vframes 1 {2}{4} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail, threads) :
+ string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads);
var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1);
var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1);
@@ -693,7 +695,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
Directory.CreateDirectory(targetDirectory);
var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg");
- var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf);
+ var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads);
var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1);
var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1);
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 6ead93e09..7bb2a7d03 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -25,7 +25,7 @@
<ItemGroup>
<PackageReference Include="BDInfo" Version="0.7.6.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
- <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
+ <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
<PackageReference Include="UTF.Unknown" Version="2.3.0" />
</ItemGroup>
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 15a70e2e7..3d3d1eb48 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -234,8 +234,8 @@ namespace MediaBrowser.MediaEncoding.Probing
var channelsValue = channels.Value;
- if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
{
if (channelsValue <= 2)
{
@@ -248,6 +248,34 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
+ if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ if (channelsValue <= 2)
+ {
+ return 192000;
+ }
+
+ if (channelsValue >= 5)
+ {
+ return 640000;
+ }
+ }
+
+ if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ if (channelsValue <= 2)
+ {
+ return 960000;
+ }
+
+ if (channelsValue >= 5)
+ {
+ return 2880000;
+ }
+ }
+
return null;
}
@@ -774,6 +802,35 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.BitRate = bitrate;
}
+ // Extract bitrate info from tag "BPS" if possible.
+ if (!stream.BitRate.HasValue
+ && (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
+ {
+ var bps = GetBPSFromTags(streamInfo);
+ if (bps != null && bps > 0)
+ {
+ stream.BitRate = bps;
+ }
+ }
+
+ // Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible.
+ if (!stream.BitRate.HasValue
+ && (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
+ {
+ var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo);
+ var bytes = GetNumberOfBytesFromTags(streamInfo);
+ if (durationInSeconds != null && bytes != null)
+ {
+ var bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture);
+ if (bps > 0)
+ {
+ stream.BitRate = bps;
+ }
+ }
+ }
+
var disposition = streamInfo.Disposition;
if (disposition != null)
{
@@ -963,6 +1020,50 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
+ private int? GetBPSFromTags(MediaStreamInfo streamInfo)
+ {
+ if (streamInfo != null && streamInfo.Tags != null)
+ {
+ var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS");
+ if (!string.IsNullOrEmpty(bps)
+ && int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
+ {
+ return parsedBps;
+ }
+ }
+
+ return null;
+ }
+
+ private double? GetRuntimeSecondsFromTags(MediaStreamInfo streamInfo)
+ {
+ if (streamInfo != null && streamInfo.Tags != null)
+ {
+ var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION");
+ if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration))
+ {
+ return parsedDuration.TotalSeconds;
+ }
+ }
+
+ return null;
+ }
+
+ private long? GetNumberOfBytesFromTags(MediaStreamInfo streamInfo)
+ {
+ if (streamInfo != null && streamInfo.Tags != null)
+ {
+ var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng") ?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES");
+ if (!string.IsNullOrEmpty(numberOfBytes)
+ && long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
+ {
+ return parsedBytes;
+ }
+ }
+
+ return null;
+ }
+
private void SetSize(InternalMediaInfoResult data, MediaInfo info)
{
if (data.Format != null)
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 0a9958b9e..b61b8a0e0 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -758,9 +758,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
case MediaProtocol.Http:
{
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetAsync(path, cancellationToken)
+ .GetAsync(new Uri(path), cancellationToken)
.ConfigureAwait(false);
- return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
}
case MediaProtocol.File:
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index c34825667..100756c24 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -67,6 +67,8 @@ namespace MediaBrowser.Model.Configuration
public bool EnableHardwareEncoding { get; set; }
+ public bool AllowHevcEncoding { get; set; }
+
public bool EnableSubtitleExtraction { get; set; }
public string[] HardwareDecodingCodecs { get; set; }
@@ -99,6 +101,7 @@ namespace MediaBrowser.Model.Configuration
EnableDecodingColorDepth10Hevc = true;
EnableDecodingColorDepth10Vp9 = true;
EnableHardwareEncoding = true;
+ AllowHevcEncoding = true;
EnableSubtitleExtraction = true;
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
}
diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
index 43407383a..c511801f4 100644
--- a/MediaBrowser.Model/Dlna/DeviceIdentification.cs
+++ b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
@@ -11,59 +11,54 @@ namespace MediaBrowser.Model.Dlna
/// Gets or sets the name of the friendly.
/// </summary>
/// <value>The name of the friendly.</value>
- public string FriendlyName { get; set; }
+ public string FriendlyName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the model number.
/// </summary>
/// <value>The model number.</value>
- public string ModelNumber { get; set; }
+ public string ModelNumber { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the serial number.
/// </summary>
/// <value>The serial number.</value>
- public string SerialNumber { get; set; }
+ public string SerialNumber { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the name of the model.
/// </summary>
/// <value>The name of the model.</value>
- public string ModelName { get; set; }
+ public string ModelName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the model description.
/// </summary>
/// <value>The model description.</value>
- public string ModelDescription { get; set; }
+ public string ModelDescription { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the model URL.
/// </summary>
/// <value>The model URL.</value>
- public string ModelUrl { get; set; }
+ public string ModelUrl { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the manufacturer.
/// </summary>
/// <value>The manufacturer.</value>
- public string Manufacturer { get; set; }
+ public string Manufacturer { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the manufacturer URL.
/// </summary>
/// <value>The manufacturer URL.</value>
- public string ManufacturerUrl { get; set; }
+ public string ManufacturerUrl { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the headers.
/// </summary>
/// <value>The headers.</value>
- public HttpHeaderInfo[] Headers { get; set; }
-
- public DeviceIdentification()
- {
- Headers = Array.Empty<HttpHeaderInfo>();
- }
+ public HttpHeaderInfo[] Headers { get; set; } = Array.Empty<HttpHeaderInfo>();
}
}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index e842efead..ff5186658 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -1,6 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
+#pragma warning disable CA1819 // Properties should not return arrays
using System;
using System.Linq;
using System.Xml.Serialization;
@@ -8,129 +7,243 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
{
+ /// <summary>
+ /// Defines the <see cref="DeviceProfile" />.
+ /// </summary>
[XmlRoot("Profile")]
public class DeviceProfile
{
/// <summary>
- /// Gets or sets the name.
+ /// Initializes a new instance of the <see cref="DeviceProfile"/> class.
+ /// </summary>
+ public DeviceProfile()
+ {
+ DirectPlayProfiles = Array.Empty<DirectPlayProfile>();
+ TranscodingProfiles = Array.Empty<TranscodingProfile>();
+ ResponseProfiles = Array.Empty<ResponseProfile>();
+ CodecProfiles = Array.Empty<CodecProfile>();
+ ContainerProfiles = Array.Empty<ContainerProfile>();
+ SubtitleProfiles = Array.Empty<SubtitleProfile>();
+
+ XmlRootAttributes = Array.Empty<XmlAttribute>();
+
+ SupportedMediaTypes = "Audio,Photo,Video";
+ MaxStreamingBitrate = 8000000;
+ MaxStaticBitrate = 8000000;
+ MusicStreamingTranscodingBitrate = 128000;
+ }
+
+ /// <summary>
+ /// Gets or sets the Name.
/// </summary>
- /// <value>The name.</value>
public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the Id.
+ /// </summary>
[XmlIgnore]
public string Id { get; set; }
/// <summary>
- /// Gets or sets the identification.
+ /// Gets or sets the Identification.
/// </summary>
- /// <value>The identification.</value>
public DeviceIdentification Identification { get; set; }
+ /// <summary>
+ /// Gets or sets the FriendlyName.
+ /// </summary>
public string FriendlyName { get; set; }
+ /// <summary>
+ /// Gets or sets the Manufacturer.
+ /// </summary>
public string Manufacturer { get; set; }
+ /// <summary>
+ /// Gets or sets the ManufacturerUrl.
+ /// </summary>
public string ManufacturerUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the ModelName.
+ /// </summary>
public string ModelName { get; set; }
+ /// <summary>
+ /// Gets or sets the ModelDescription.
+ /// </summary>
public string ModelDescription { get; set; }
+ /// <summary>
+ /// Gets or sets the ModelNumber.
+ /// </summary>
public string ModelNumber { get; set; }
+ /// <summary>
+ /// Gets or sets the ModelUrl.
+ /// </summary>
public string ModelUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the SerialNumber.
+ /// </summary>
public string SerialNumber { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether EnableAlbumArtInDidl.
+ /// </summary>
public bool EnableAlbumArtInDidl { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether EnableSingleAlbumArtLimit.
+ /// </summary>
public bool EnableSingleAlbumArtLimit { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether EnableSingleSubtitleLimit.
+ /// </summary>
public bool EnableSingleSubtitleLimit { get; set; }
+ /// <summary>
+ /// Gets or sets the SupportedMediaTypes.
+ /// </summary>
public string SupportedMediaTypes { get; set; }
+ /// <summary>
+ /// Gets or sets the UserId.
+ /// </summary>
public string UserId { get; set; }
+ /// <summary>
+ /// Gets or sets the AlbumArtPn.
+ /// </summary>
public string AlbumArtPn { get; set; }
+ /// <summary>
+ /// Gets or sets the MaxAlbumArtWidth.
+ /// </summary>
public int MaxAlbumArtWidth { get; set; }
+ /// <summary>
+ /// Gets or sets the MaxAlbumArtHeight.
+ /// </summary>
public int MaxAlbumArtHeight { get; set; }
+ /// <summary>
+ /// Gets or sets the MaxIconWidth.
+ /// </summary>
public int? MaxIconWidth { get; set; }
+ /// <summary>
+ /// Gets or sets the MaxIconHeight.
+ /// </summary>
public int? MaxIconHeight { get; set; }
+ /// <summary>
+ /// Gets or sets the MaxStreamingBitrate.
+ /// </summary>
public int? MaxStreamingBitrate { get; set; }
+ /// <summary>
+ /// Gets or sets the MaxStaticBitrate.
+ /// </summary>
public int? MaxStaticBitrate { get; set; }
+ /// <summary>
+ /// Gets or sets the MusicStreamingTranscodingBitrate.
+ /// </summary>
public int? MusicStreamingTranscodingBitrate { get; set; }
+ /// <summary>
+ /// Gets or sets the MaxStaticMusicBitrate.
+ /// </summary>
public int? MaxStaticMusicBitrate { get; set; }
/// <summary>
- /// Controls the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace.
+ /// Gets or sets the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace.
/// </summary>
public string SonyAggregationFlags { get; set; }
+ /// <summary>
+ /// Gets or sets the ProtocolInfo.
+ /// </summary>
public string ProtocolInfo { get; set; }
+ /// <summary>
+ /// Gets or sets the TimelineOffsetSeconds.
+ /// </summary>
public int TimelineOffsetSeconds { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether RequiresPlainVideoItems.
+ /// </summary>
public bool RequiresPlainVideoItems { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether RequiresPlainFolders.
+ /// </summary>
public bool RequiresPlainFolders { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether EnableMSMediaReceiverRegistrar.
+ /// </summary>
public bool EnableMSMediaReceiverRegistrar { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether IgnoreTranscodeByteRangeRequests.
+ /// </summary>
public bool IgnoreTranscodeByteRangeRequests { get; set; }
+ /// <summary>
+ /// Gets or sets the XmlRootAttributes.
+ /// </summary>
public XmlAttribute[] XmlRootAttributes { get; set; }
/// <summary>
/// Gets or sets the direct play profiles.
/// </summary>
- /// <value>The direct play profiles.</value>
public DirectPlayProfile[] DirectPlayProfiles { get; set; }
/// <summary>
/// Gets or sets the transcoding profiles.
/// </summary>
- /// <value>The transcoding profiles.</value>
public TranscodingProfile[] TranscodingProfiles { get; set; }
+ /// <summary>
+ /// Gets or sets the ContainerProfiles.
+ /// </summary>
public ContainerProfile[] ContainerProfiles { get; set; }
+ /// <summary>
+ /// Gets or sets the CodecProfiles.
+ /// </summary>
public CodecProfile[] CodecProfiles { get; set; }
+ /// <summary>
+ /// Gets or sets the ResponseProfiles.
+ /// </summary>
public ResponseProfile[] ResponseProfiles { get; set; }
+ /// <summary>
+ /// Gets or sets the SubtitleProfiles.
+ /// </summary>
public SubtitleProfile[] SubtitleProfiles { get; set; }
- public DeviceProfile()
- {
- DirectPlayProfiles = Array.Empty<DirectPlayProfile>();
- TranscodingProfiles = Array.Empty<TranscodingProfile>();
- ResponseProfiles = Array.Empty<ResponseProfile>();
- CodecProfiles = Array.Empty<CodecProfile>();
- ContainerProfiles = Array.Empty<ContainerProfile>();
- SubtitleProfiles = Array.Empty<SubtitleProfile>();
-
- XmlRootAttributes = Array.Empty<XmlAttribute>();
-
- SupportedMediaTypes = "Audio,Photo,Video";
- MaxStreamingBitrate = 8000000;
- MaxStaticBitrate = 8000000;
- MusicStreamingTranscodingBitrate = 128000;
- }
-
+ /// <summary>
+ /// The GetSupportedMediaTypes.
+ /// </summary>
+ /// <returns>The .</returns>
public string[] GetSupportedMediaTypes()
{
return ContainerProfile.SplitValue(SupportedMediaTypes);
}
+ /// <summary>
+ /// Gets the audio transcoding profile.
+ /// </summary>
+ /// <param name="container">The container.</param>
+ /// <param name="audioCodec">The audio Codec.</param>
+ /// <returns>A <see cref="TranscodingProfile"/>.</returns>
public TranscodingProfile GetAudioTranscodingProfile(string container, string audioCodec)
{
container = (container ?? string.Empty).TrimStart('.');
@@ -158,6 +271,13 @@ namespace MediaBrowser.Model.Dlna
return null;
}
+ /// <summary>
+ /// Gets the video transcoding profile.
+ /// </summary>
+ /// <param name="container">The container.</param>
+ /// <param name="audioCodec">The audio Codec.</param>
+ /// <param name="videoCodec">The video Codec.</param>
+ /// <returns>The <see cref="TranscodingProfile"/>.</returns>
public TranscodingProfile GetVideoTranscodingProfile(string container, string audioCodec, string videoCodec)
{
container = (container ?? string.Empty).TrimStart('.');
@@ -190,6 +310,16 @@ namespace MediaBrowser.Model.Dlna
return null;
}
+ /// <summary>
+ /// Gets the audio media profile.
+ /// </summary>
+ /// <param name="container">The container.</param>
+ /// <param name="audioCodec">The audio codec.</param>
+ /// <param name="audioChannels">The audio channels.</param>
+ /// <param name="audioBitrate">The audio bitrate.</param>
+ /// <param name="audioSampleRate">The audio sample rate.</param>
+ /// <param name="audioBitDepth">The audio bit depth.</param>
+ /// <returns>The <see cref="ResponseProfile"/>.</returns>
public ResponseProfile GetAudioMediaProfile(string container, string audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
{
foreach (var i in ResponseProfiles)
@@ -231,6 +361,11 @@ namespace MediaBrowser.Model.Dlna
return null;
}
+ /// <summary>
+ /// Gets the model profile condition.
+ /// </summary>
+ /// <param name="c">The c<see cref="ProfileCondition"/>.</param>
+ /// <returns>The <see cref="ProfileCondition"/>.</returns>
private ProfileCondition GetModelProfileCondition(ProfileCondition c)
{
return new ProfileCondition
@@ -242,6 +377,13 @@ namespace MediaBrowser.Model.Dlna
};
}
+ /// <summary>
+ /// Gets the image media profile.
+ /// </summary>
+ /// <param name="container">The container.</param>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ /// <returns>The <see cref="ResponseProfile"/>.</returns>
public ResponseProfile GetImageMediaProfile(string container, int? width, int? height)
{
foreach (var i in ResponseProfiles)
@@ -277,6 +419,29 @@ namespace MediaBrowser.Model.Dlna
return null;
}
+ /// <summary>
+ /// Gets the video media profile.
+ /// </summary>
+ /// <param name="container">The container.</param>
+ /// <param name="audioCodec">The audio codec.</param>
+ /// <param name="videoCodec">The video codec.</param>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ /// <param name="bitDepth">The bit depth.</param>
+ /// <param name="videoBitrate">The video bitrate.</param>
+ /// <param name="videoProfile">The video profile.</param>
+ /// <param name="videoLevel">The video level.</param>
+ /// <param name="videoFramerate">The video framerate.</param>
+ /// <param name="packetLength">The packet length.</param>
+ /// <param name="timestamp">The timestamp<see cref="TransportStreamTimestamp"/>.</param>
+ /// <param name="isAnamorphic">True if anamorphic.</param>
+ /// <param name="isInterlaced">True if interlaced.</param>
+ /// <param name="refFrames">The ref frames.</param>
+ /// <param name="numVideoStreams">The number of video streams.</param>
+ /// <param name="numAudioStreams">The number of audio streams.</param>
+ /// <param name="videoCodecTag">The video Codec tag.</param>
+ /// <param name="isAvc">True if Avc.</param>
+ /// <returns>The <see cref="ResponseProfile"/>.</returns>
public ResponseProfile GetVideoMediaProfile(
string container,
string audioCodec,
diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
index a4305c810..65fccbdd4 100644
--- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
+++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Dlna
new ResolutionConfiguration(720, 950000),
new ResolutionConfiguration(1280, 2500000),
new ResolutionConfiguration(1920, 4000000),
- new ResolutionConfiguration(2560, 8000000),
+ new ResolutionConfiguration(2560, 20000000),
new ResolutionConfiguration(3840, 35000000)
};
@@ -29,7 +29,7 @@ namespace MediaBrowser.Model.Dlna
int? maxWidth,
int? maxHeight)
{
- // If the bitrate isn't changing, then don't downlscale the resolution
+ // If the bitrate isn't changing, then don't downscale the resolution
if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value)
{
if (maxWidth.HasValue || maxHeight.HasValue)
@@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Dlna
private static double GetVideoBitrateScaleFactor(string codec)
{
- if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
- return .5;
+ return .6;
}
return 1;
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 13234c381..59c981000 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -872,11 +872,34 @@ namespace MediaBrowser.Model.Dlna
return playlistItem;
}
- private static int GetDefaultAudioBitrateIfUnknown(MediaStream audioStream)
+ private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels)
{
- if ((audioStream.Channels ?? 0) >= 6)
+ if (!string.IsNullOrEmpty(audioCodec))
{
- return 384000;
+ // Default to a higher bitrate for stream copy
+ if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ if ((audioChannels ?? 0) < 2)
+ {
+ return 128000;
+ }
+
+ return (audioChannels ?? 0) >= 6 ? 640000 : 384000;
+ }
+
+ if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ if ((audioChannels ?? 0) < 2)
+ {
+ return 768000;
+ }
+
+ return (audioChannels ?? 0) >= 6 ? 3584000 : 1536000;
+ }
}
return 192000;
@@ -897,14 +920,27 @@ namespace MediaBrowser.Model.Dlna
}
else
{
- if (targetAudioChannels.HasValue && audioStream.Channels.HasValue && targetAudioChannels.Value < audioStream.Channels.Value)
+ if (targetAudioChannels.HasValue
+ && audioStream.Channels.HasValue
+ && audioStream.Channels.Value > targetAudioChannels.Value)
{
- // Reduce the bitrate if we're downmixing
- defaultBitrate = targetAudioChannels.Value < 2 ? 128000 : 192000;
+ // Reduce the bitrate if we're downmixing.
+ defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
+ }
+ else if (targetAudioChannels.HasValue
+ && audioStream.Channels.HasValue
+ && audioStream.Channels.Value <= targetAudioChannels.Value
+ && !string.IsNullOrEmpty(audioStream.Codec)
+ && targetAudioCodecs != null
+ && targetAudioCodecs.Length > 0
+ && !Array.Exists(targetAudioCodecs, elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase)))
+ {
+ // Shift the bitrate if we're transcoding to a different audio codec.
+ defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value);
}
else
{
- defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrateIfUnknown(audioStream);
+ defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
}
// Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
@@ -938,8 +974,28 @@ namespace MediaBrowser.Model.Dlna
{
return 448000;
}
+ else if (totalBitrate <= 4000000)
+ {
+ return 640000;
+ }
+ else if (totalBitrate <= 5000000)
+ {
+ return 768000;
+ }
+ else if (totalBitrate <= 10000000)
+ {
+ return 1536000;
+ }
+ else if (totalBitrate <= 15000000)
+ {
+ return 2304000;
+ }
+ else if (totalBitrate <= 20000000)
+ {
+ return 3584000;
+ }
- return 640000;
+ return 7168000;
}
private (PlayMethod?, List<TranscodeReason>) GetVideoDirectPlayProfile(
@@ -1438,6 +1494,32 @@ namespace MediaBrowser.Model.Dlna
break;
}
+ case ProfileConditionValue.AudioSampleRate:
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.AudioSampleRate = num;
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.AudioSampleRate = Math.Min(num, item.AudioSampleRate ?? num);
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.AudioSampleRate = Math.Max(num, item.AudioSampleRate ?? num);
+ }
+ }
+
+ break;
+ }
+
case ProfileConditionValue.AudioChannels:
{
if (string.IsNullOrEmpty(qualifier))
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 9399d21f1..55b12ae81 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -110,6 +110,8 @@ namespace MediaBrowser.Model.Dlna
public int? AudioBitrate { get; set; }
+ public int? AudioSampleRate { get; set; }
+
public int? VideoBitrate { get; set; }
public int? MaxWidth { get; set; }
@@ -183,8 +185,10 @@ namespace MediaBrowser.Model.Dlna
continue;
}
+ // Be careful, IsDirectStream==true by default (Static != false or not in query).
+ // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
+ string.Equals(pair.Value, "true", StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -250,6 +254,7 @@ namespace MediaBrowser.Model.Dlna
list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
@@ -521,7 +526,9 @@ namespace MediaBrowser.Model.Dlna
get
{
var stream = TargetAudioStream;
- return stream == null ? null : stream.SampleRate;
+ return AudioSampleRate.HasValue && !IsDirectStream
+ ? AudioSampleRate
+ : stream == null ? null : stream.SampleRate;
}
}
@@ -787,7 +794,7 @@ namespace MediaBrowser.Model.Dlna
public int? GetTargetAudioChannels(string codec)
{
- var defaultValue = GlobalMaxAudioChannels;
+ var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
var value = GetOption(codec, "audiochannels");
if (string.IsNullOrEmpty(value))
diff --git a/MediaBrowser.Model/Dlna/XmlAttribute.cs b/MediaBrowser.Model/Dlna/XmlAttribute.cs
index 3a8939a79..03bb2e4b1 100644
--- a/MediaBrowser.Model/Dlna/XmlAttribute.cs
+++ b/MediaBrowser.Model/Dlna/XmlAttribute.cs
@@ -5,11 +5,20 @@ using System.Xml.Serialization;
namespace MediaBrowser.Model.Dlna
{
+ /// <summary>
+ /// Defines the <see cref="XmlAttribute" />.
+ /// </summary>
public class XmlAttribute
{
+ /// <summary>
+ /// Gets or sets the name of the attribute.
+ /// </summary>
[XmlAttribute("name")]
public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the value of the attribute.
+ /// </summary>
[XmlAttribute("value")]
public string Value { get; set; }
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index fa3c9aaa2..ca0b93c30 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -191,6 +191,11 @@ namespace MediaBrowser.Model.Entities
attributes.Add(Codec.ToUpperInvariant());
}
+ if (!string.IsNullOrEmpty(VideoRange))
+ {
+ attributes.Add(VideoRange.ToUpperInvariant());
+ }
+
if (!string.IsNullOrEmpty(Title))
{
var result = new StringBuilder(Title);
diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
index 9c11fe0ad..1782b42e2 100644
--- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
+++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
@@ -48,7 +48,7 @@ namespace MediaBrowser.Model.Entities
return null;
}
- instance.ProviderIds.TryGetValue(name, out string id);
+ instance.ProviderIds.TryGetValue(name, out string? id);
return id;
}
diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs
index 8ffa3c4ba..2d9a6c4db 100644
--- a/MediaBrowser.Model/Extensions/StringHelper.cs
+++ b/MediaBrowser.Model/Extensions/StringHelper.cs
@@ -22,11 +22,6 @@ namespace MediaBrowser.Model.Extensions
return str;
}
-#if NETSTANDARD2_0
- char[] a = str.ToCharArray();
- a[0] = char.ToUpperInvariant(a[0]);
- return new string(a);
-#else
return string.Create(
str.Length,
str,
@@ -38,7 +33,6 @@ namespace MediaBrowser.Model.Extensions
chars[i] = buf[i];
}
});
-#endif
}
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 253ee7e79..b86187f9b 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -14,7 +14,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
@@ -32,11 +32,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
+ <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.9" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
- <PackageReference Include="System.Text.Json" Version="5.0.0-preview.8.20407.11" />
+ <PackageReference Include="System.Text.Json" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Model/Net/HttpException.cs b/MediaBrowser.Model/Net/HttpException.cs
deleted file mode 100644
index 48ff5d51c..000000000
--- a/MediaBrowser.Model/Net/HttpException.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System;
-using System.Net;
-
-namespace MediaBrowser.Model.Net
-{
- /// <summary>
- /// Class HttpException.
- /// </summary>
- public class HttpException : Exception
- {
- /// <summary>
- /// Gets or sets the status code.
- /// </summary>
- /// <value>The status code.</value>
- public HttpStatusCode? StatusCode { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is timed out.
- /// </summary>
- /// <value><c>true</c> if this instance is timed out; otherwise, <c>false</c>.</value>
- public bool IsTimedOut { get; set; }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpException" /> class.
- /// </summary>
- /// <param name="message">The message.</param>
- /// <param name="innerException">The inner exception.</param>
- public HttpException(string message, Exception innerException)
- : base(message, innerException)
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpException" /> class.
- /// </summary>
- /// <param name="message">The message.</param>
- public HttpException(string message)
- : base(message)
- {
- }
- }
-}
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index afe7351d3..902db1e9e 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -177,7 +177,7 @@ namespace MediaBrowser.Model.Net
var ext = Path.GetExtension(path);
- if (_mimeTypeLookup.TryGetValue(ext, out string result))
+ if (_mimeTypeLookup.TryGetValue(ext, out string? result))
{
return result;
}
@@ -210,9 +210,9 @@ namespace MediaBrowser.Model.Net
return enableStreamDefault ? "application/octet-stream" : null;
}
- public static string? ToExtension(string mimeType)
+ public static string? ToExtension(string? mimeType)
{
- if (mimeType.Length == 0)
+ if (string.IsNullOrEmpty(mimeType))
{
throw new ArgumentException("String can't be empty.", nameof(mimeType));
}
@@ -220,7 +220,7 @@ namespace MediaBrowser.Model.Net
// handle text/html; charset=UTF-8
mimeType = mimeType.Split(';')[0];
- if (_extensionLookup.TryGetValue(mimeType, out string result))
+ if (_extensionLookup.TryGetValue(mimeType, out string? result))
{
return result;
}
diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
index ef435b21e..e8ee49403 100644
--- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
+++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
namespace MediaBrowser.Model.Playlists
{
@@ -9,15 +10,10 @@ namespace MediaBrowser.Model.Playlists
{
public string Name { get; set; }
- public Guid[] ItemIdList { get; set; }
+ public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>();
public string MediaType { get; set; }
public Guid UserId { get; set; }
-
- public PlaylistCreationRequest()
- {
- ItemIdList = Array.Empty<Guid>();
- }
}
}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index a57a85376..ffc6889fa 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -468,7 +469,7 @@ namespace MediaBrowser.Providers.Manager
try
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await _providerManager.SaveImage(
item,
@@ -481,7 +482,7 @@ namespace MediaBrowser.Providers.Manager
result.UpdateType |= ItemUpdateType.ImageUpdate;
return true;
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
// Sometimes providers send back bad url's. Just move to the next image
if (ex.StatusCode.HasValue
@@ -585,7 +586,7 @@ namespace MediaBrowser.Providers.Manager
}
}
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await _providerManager.SaveImage(
item,
stream,
@@ -595,7 +596,7 @@ namespace MediaBrowser.Providers.Manager
cancellationToken).ConfigureAwait(false);
result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
}
- catch (HttpException ex)
+ catch (HttpRequestException ex)
{
// Sometimes providers send back bad urls. Just move onto the next image
if (ex.StatusCode.HasValue
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index a0c7d4ad0..e7e44876d 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -13,6 +13,7 @@ using Jellyfin.Data.Events;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -51,6 +52,7 @@ namespace MediaBrowser.Providers.Manager
private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subtitleManager;
private readonly IServerConfigurationManager _configurationManager;
+ private readonly IBaseItemManager _baseItemManager;
private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>();
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
@@ -74,6 +76,7 @@ namespace MediaBrowser.Providers.Manager
/// <param name="fileSystem">The filesystem.</param>
/// <param name="appPaths">The server application paths.</param>
/// <param name="libraryManager">The library manager.</param>
+ /// <param name="baseItemManager">The BaseItem manager.</param>
public ProviderManager(
IHttpClientFactory httpClientFactory,
ISubtitleManager subtitleManager,
@@ -82,7 +85,8 @@ namespace MediaBrowser.Providers.Manager
ILogger<ProviderManager> logger,
IFileSystem fileSystem,
IServerApplicationPaths appPaths,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ IBaseItemManager baseItemManager)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
@@ -92,6 +96,7 @@ namespace MediaBrowser.Providers.Manager
_appPaths = appPaths;
_libraryManager = libraryManager;
_subtitleManager = subtitleManager;
+ _baseItemManager = baseItemManager;
}
/// <inheritdoc/>
@@ -160,10 +165,7 @@ namespace MediaBrowser.Providers.Manager
if (response.StatusCode != HttpStatusCode.OK)
{
- throw new HttpException("Invalid image received.")
- {
- StatusCode = response.StatusCode
- };
+ throw new HttpRequestException("Invalid image received.", null, response.StatusCode);
}
var contentType = response.Content.Headers.ContentType.MediaType;
@@ -181,13 +183,10 @@ namespace MediaBrowser.Providers.Manager
// thetvdb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
{
- throw new HttpException("Invalid image received.")
- {
- StatusCode = HttpStatusCode.NotFound
- };
+ throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound);
}
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await SaveImage(
item,
stream,
@@ -398,7 +397,7 @@ namespace MediaBrowser.Providers.Manager
if (provider is IRemoteMetadataProvider)
{
- if (!forceEnableInternetMetadata && !item.IsMetadataFetcherEnabled(libraryOptions, provider.Name))
+ if (!forceEnableInternetMetadata && !_baseItemManager.IsMetadataFetcherEnabled(item, libraryOptions, provider.Name))
{
return false;
}
@@ -442,7 +441,7 @@ namespace MediaBrowser.Providers.Manager
if (provider is IRemoteImageProvider || provider is IDynamicImageProvider)
{
- if (!item.IsImageFetcherEnabled(libraryOptions, provider.Name))
+ if (!_baseItemManager.IsImageFetcherEnabled(item, libraryOptions, provider.Name))
{
return false;
}
diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs
index 70a5a6ac1..5621d2b86 100644
--- a/MediaBrowser.Providers/Manager/ProviderUtils.cs
+++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs
@@ -38,7 +38,7 @@ namespace MediaBrowser.Providers.Manager
{
if (replaceData || string.IsNullOrEmpty(target.Name))
{
- // Safeguard against incoming data having an emtpy name
+ // Safeguard against incoming data having an empty name
if (!string.IsNullOrWhiteSpace(source.Name))
{
target.Name = source.Name;
@@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Manager
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
{
- // Safeguard against incoming data having an emtpy name
+ // Safeguard against incoming data having an empty name
if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
{
target.OriginalTitle = source.OriginalTitle;
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 9465fe42c..accdea36e 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,17 +16,17 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
- <PackageReference Include="PlaylistsNET" Version="1.1.2" />
+ <PackageReference Include="PlaylistsNET" Version="1.1.3" />
<PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
<PackageReference Include="TvDbSharper" Version="3.2.2" />
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
index e6d89e688..6536b303f 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
@@ -175,7 +175,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
index 72dad8a25..85c92fa7b 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
@@ -156,7 +156,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(path));
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
index f27da7ce6..dc755b600 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
@@ -38,7 +38,7 @@ namespace MediaBrowser.Providers.Music
var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture);
using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return GetResultsFromResponse(stream);
}
else
@@ -49,7 +49,7 @@ namespace MediaBrowser.Providers.Music
var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch));
using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
- await using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
+ await using (var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
{
var results = GetResultsFromResponse(stream).ToList();
@@ -65,7 +65,7 @@ namespace MediaBrowser.Providers.Music
url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch));
using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return GetResultsFromResponse(stream);
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 31f0123dc..93178d64a 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -125,7 +125,7 @@ namespace MediaBrowser.Providers.Music
if (!string.IsNullOrWhiteSpace(url))
{
using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return GetResultsFromResponse(stream);
}
@@ -284,7 +284,7 @@ namespace MediaBrowser.Providers.Music
artistId);
using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var oReader = new StreamReader(stream, Encoding.UTF8);
var settings = new XmlReaderSettings
{
@@ -307,7 +307,7 @@ namespace MediaBrowser.Providers.Music
WebUtility.UrlEncode(artistName));
using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var oReader = new StreamReader(stream, Encoding.UTF8);
var settings = new XmlReaderSettings()
{
@@ -622,7 +622,7 @@ namespace MediaBrowser.Providers.Music
var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture);
using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var oReader = new StreamReader(stream, Encoding.UTF8);
var settings = new XmlReaderSettings
{
@@ -649,7 +649,7 @@ namespace MediaBrowser.Providers.Music
var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture);
using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var oReader = new StreamReader(stream, Encoding.UTF8);
var settings = new XmlReaderSettings
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 705359d2c..43d8af75f 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -133,7 +133,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var url = OmdbProvider.GetOmdbUrl(urlQuery);
using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var resultList = new List<SearchResult>();
if (isSearch)
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 9eed6172d..332479ff8 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -50,7 +50,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
- // Only take the name and rating if the user's language is set to english, since Omdb has no localization
+ // Only take the name and rating if the user's language is set to English, since Omdb has no localization
if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
{
item.Name = result.Title;
@@ -151,7 +151,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return false;
}
- // Only take the name and rating if the user's language is set to english, since Omdb has no localization
+ // Only take the name and rating if the user's language is set to English, since Omdb has no localization
if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
{
item.Name = result.Title;
@@ -298,7 +298,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam));
using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(path));
_jsonSerializer.SerializeToFile(rootObject, path);
@@ -336,7 +336,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
seasonId));
using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(path));
_jsonSerializer.SerializeToFile(rootObject, path);
@@ -385,7 +385,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport;
// Grab series genres because IMDb data is better than TVDB. Leave movies alone
- // But only do it if english is the preferred language because this data will not be localized
+ // But only do it if English is the preferred language because this data will not be localized
if (isConfiguredForEnglish && !string.IsNullOrWhiteSpace(result.Genre))
{
item.Genres = Array.Empty<string>();
@@ -401,7 +401,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (isConfiguredForEnglish)
{
- // Omdb is currently english only, so for other languages skip this and let secondary providers fill it in
+ // Omdb is currently English only, so for other languages skip this and let secondary providers fill it in
item.Overview = result.Plot;
}
@@ -455,7 +455,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var lang = item.GetPreferredMetadataLanguage();
- // The data isn't localized and so can only be used for english users
+ // The data isn't localized and so can only be used for English users
return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase);
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs
deleted file mode 100644
index 690a52c4d..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Model.Plugins;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public class PluginConfiguration : BasePluginConfiguration
- {
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
deleted file mode 100644
index e7079ed3c..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Model.Serialization;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public class Plugin : BasePlugin<PluginConfiguration>
- {
- public static Plugin Instance { get; private set; }
-
- public override Guid Id => new Guid("a677c0da-fac5-4cde-941a-7134223f14c8");
-
- public override string Name => "TheTVDB";
-
- public override string Description => "Get metadata for movies and other video content from TheTVDB.";
-
- // TODO remove when plugin removed from server.
- public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml";
-
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
- : base(applicationPaths, xmlSerializer)
- {
- Instance = this;
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
deleted file mode 100644
index ce0dab701..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
+++ /dev/null
@@ -1,289 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using Microsoft.Extensions.Caching.Memory;
-using TvDbSharper;
-using TvDbSharper.Dto;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public class TvdbClientManager
- {
- private const string DefaultLanguage = "en";
-
- private readonly IMemoryCache _cache;
- private readonly TvDbClient _tvDbClient;
- private DateTime _tokenCreatedAt;
-
- public TvdbClientManager(IMemoryCache memoryCache)
- {
- _cache = memoryCache;
- _tvDbClient = new TvDbClient();
- }
-
- private TvDbClient TvDbClient
- {
- get
- {
- if (string.IsNullOrEmpty(_tvDbClient.Authentication.Token))
- {
- _tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
- _tokenCreatedAt = DateTime.Now;
- }
-
- // Refresh if necessary
- if (_tokenCreatedAt < DateTime.Now.Subtract(TimeSpan.FromHours(20)))
- {
- try
- {
- _tvDbClient.Authentication.RefreshTokenAsync().GetAwaiter().GetResult();
- }
- catch
- {
- _tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
- }
-
- _tokenCreatedAt = DateTime.Now;
- }
-
- return _tvDbClient;
- }
- }
-
- public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByNameAsync(string name, string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey("series", name, language);
- return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByNameAsync(name, cancellationToken));
- }
-
- public Task<TvDbResponse<Series>> GetSeriesByIdAsync(int tvdbId, string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey("series", tvdbId, language);
- return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetAsync(tvdbId, cancellationToken));
- }
-
- public Task<TvDbResponse<EpisodeRecord>> GetEpisodesAsync(int episodeTvdbId, string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey("episode", episodeTvdbId, language);
- return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken));
- }
-
- public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync(
- string imdbId,
- string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey("series", imdbId, language);
- return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken));
- }
-
- public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByZap2ItIdAsync(
- string zap2ItId,
- string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey("series", zap2ItId, language);
- return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken));
- }
-
- public Task<TvDbResponse<Actor[]>> GetActorsAsync(
- int tvdbId,
- string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey("actors", tvdbId, language);
- return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetActorsAsync(tvdbId, cancellationToken));
- }
-
- public Task<TvDbResponse<Image[]>> GetImagesAsync(
- int tvdbId,
- ImagesQuery imageQuery,
- string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey("images", tvdbId, language, imageQuery);
- return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesAsync(tvdbId, imageQuery, cancellationToken));
- }
-
- public Task<TvDbResponse<Language[]>> GetLanguagesAsync(CancellationToken cancellationToken)
- {
- return TryGetValue("languages", null, () => TvDbClient.Languages.GetAllAsync(cancellationToken));
- }
-
- public Task<TvDbResponse<EpisodesSummary>> GetSeriesEpisodeSummaryAsync(
- int tvdbId,
- string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey("seriesepisodesummary", tvdbId, language);
- return TryGetValue(cacheKey, language,
- () => TvDbClient.Series.GetEpisodesSummaryAsync(tvdbId, cancellationToken));
- }
-
- public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(
- int tvdbId,
- int page,
- EpisodeQuery episodeQuery,
- string language,
- CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey(language, tvdbId, episodeQuery);
-
- return TryGetValue(cacheKey, language,
- () => TvDbClient.Series.GetEpisodesAsync(tvdbId, page, episodeQuery, cancellationToken));
- }
-
- public Task<string> GetEpisodeTvdbId(
- EpisodeInfo searchInfo,
- string language,
- CancellationToken cancellationToken)
- {
- searchInfo.SeriesProviderIds.TryGetValue(
- nameof(MetadataProvider.Tvdb),
- out var seriesTvdbId);
-
- var episodeQuery = new EpisodeQuery();
-
- // Prefer SxE over premiere date as it is more robust
- if (searchInfo.IndexNumber.HasValue && searchInfo.ParentIndexNumber.HasValue)
- {
- switch (searchInfo.SeriesDisplayOrder)
- {
- case "dvd":
- episodeQuery.DvdEpisode = searchInfo.IndexNumber.Value;
- episodeQuery.DvdSeason = searchInfo.ParentIndexNumber.Value;
- break;
- case "absolute":
- episodeQuery.AbsoluteNumber = searchInfo.IndexNumber.Value;
- break;
- default:
- // aired order
- episodeQuery.AiredEpisode = searchInfo.IndexNumber.Value;
- episodeQuery.AiredSeason = searchInfo.ParentIndexNumber.Value;
- break;
- }
- }
- else if (searchInfo.PremiereDate.HasValue)
- {
- // tvdb expects yyyy-mm-dd format
- episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
- }
-
- return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), episodeQuery, language, cancellationToken);
- }
-
- public async Task<string> GetEpisodeTvdbId(
- int seriesTvdbId,
- EpisodeQuery episodeQuery,
- string language,
- CancellationToken cancellationToken)
- {
- var episodePage =
- await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken)
- .ConfigureAwait(false);
- return episodePage.Data.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
- }
-
- public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(
- int tvdbId,
- EpisodeQuery episodeQuery,
- string language,
- CancellationToken cancellationToken)
- {
- return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
- }
-
- public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
- var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
-
- if (imagesSummary.Data.Fanart > 0)
- {
- yield return KeyType.Fanart;
- }
-
- if (imagesSummary.Data.Series > 0)
- {
- yield return KeyType.Series;
- }
-
- if (imagesSummary.Data.Poster > 0)
- {
- yield return KeyType.Poster;
- }
- }
-
- public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
- {
- var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
- var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
-
- if (imagesSummary.Data.Season > 0)
- {
- yield return KeyType.Season;
- }
-
- if (imagesSummary.Data.Fanart > 0)
- {
- yield return KeyType.Fanart;
- }
-
- // TODO seasonwide is not supported in TvDbSharper
- }
-
- private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
- {
- if (_cache.TryGetValue(key, out T cachedValue))
- {
- return cachedValue;
- }
-
- _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage;
- var result = await resultFactory.Invoke().ConfigureAwait(false);
- _cache.Set(key, result, TimeSpan.FromHours(1));
- return result;
- }
-
- private static string GenerateKey(params object[] objects)
- {
- var key = string.Empty;
-
- foreach (var obj in objects)
- {
- var objType = obj.GetType();
- if (objType.IsPrimitive || objType == typeof(string))
- {
- key += obj + ";";
- }
- else
- {
- foreach (PropertyInfo propertyInfo in objType.GetProperties())
- {
- var currentValue = propertyInfo.GetValue(obj, null);
- if (currentValue == null)
- {
- continue;
- }
-
- key += propertyInfo.Name + "=" + currentValue + ";";
- }
- }
- }
-
- return key;
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
deleted file mode 100644
index 50a876d6c..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using Microsoft.Extensions.Logging;
-using TvDbSharper;
-using TvDbSharper.Dto;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public class TvdbEpisodeImageProvider : IRemoteImageProvider
- {
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<TvdbEpisodeImageProvider> _logger;
- private readonly TvdbClientManager _tvdbClientManager;
-
- public TvdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager)
- {
- _httpClientFactory = httpClientFactory;
- _logger = logger;
- _tvdbClientManager = tvdbClientManager;
- }
-
- public string Name => "TheTVDB";
-
- public bool Supports(BaseItem item)
- {
- return item is Episode;
- }
-
- public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
- {
- return new List<ImageType>
- {
- ImageType.Primary
- };
- }
-
- public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
- {
- var episode = (Episode)item;
- var series = episode.Series;
- var imageResult = new List<RemoteImageInfo>();
- var language = item.GetPreferredMetadataLanguage();
- if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
- {
- // Process images
- try
- {
- string episodeTvdbId = null;
-
- if (episode.IndexNumber.HasValue && episode.ParentIndexNumber.HasValue)
- {
- 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}",
- episode.ParentIndexNumber,
- episode.IndexNumber,
- series.GetProviderId(MetadataProvider.Tvdb));
- return imageResult;
- }
-
- var episodeResult =
- await _tvdbClientManager
- .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId, CultureInfo.InvariantCulture), language, cancellationToken)
- .ConfigureAwait(false);
-
- var image = GetImageInfo(episodeResult.Data);
- if (image != null)
- {
- imageResult.Add(image);
- }
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProvider.Tvdb));
- }
- }
-
- return imageResult;
- }
-
- private RemoteImageInfo GetImageInfo(EpisodeRecord episode)
- {
- if (string.IsNullOrEmpty(episode.Filename))
- {
- return null;
- }
-
- return new RemoteImageInfo
- {
- Width = Convert.ToInt32(episode.ThumbWidth, CultureInfo.InvariantCulture),
- Height = Convert.ToInt32(episode.ThumbHeight, CultureInfo.InvariantCulture),
- ProviderName = Name,
- Url = TvdbUtils.BannerUrl + episode.Filename,
- Type = ImageType.Primary
- };
- }
-
- 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/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
deleted file mode 100644
index fd72ea4a8..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
+++ /dev/null
@@ -1,262 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using Microsoft.Extensions.Logging;
-using TvDbSharper;
-using TvDbSharper.Dto;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- /// <summary>
- /// Class RemoteEpisodeProvider.
- /// </summary>
- public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
- {
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<TvdbEpisodeProvider> _logger;
- private readonly TvdbClientManager _tvdbClientManager;
-
- public TvdbEpisodeProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager)
- {
- _httpClientFactory = httpClientFactory;
- _logger = logger;
- _tvdbClientManager = tvdbClientManager;
- }
-
- public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
- {
- var list = new List<RemoteSearchResult>();
-
- // Either an episode number or date must be provided; and the dictionary of provider ids must be valid
- if ((searchInfo.IndexNumber == null && searchInfo.PremiereDate == null)
- || !TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds))
- {
- return list;
- }
-
- var metadataResult = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false);
-
- if (!metadataResult.HasMetadata)
- {
- return list;
- }
-
- var item = metadataResult.Item;
-
- list.Add(new RemoteSearchResult
- {
- IndexNumber = item.IndexNumber,
- Name = item.Name,
- ParentIndexNumber = item.ParentIndexNumber,
- PremiereDate = item.PremiereDate,
- ProductionYear = item.ProductionYear,
- ProviderIds = item.ProviderIds,
- SearchProviderName = Name,
- IndexNumberEnd = item.IndexNumberEnd
- });
-
- return list;
- }
-
- public string Name => "TheTVDB";
-
- public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo searchInfo, CancellationToken cancellationToken)
- {
- var result = new MetadataResult<Episode>
- {
- QueriedById = true
- };
-
- if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) &&
- (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue))
- {
- result = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name);
- }
-
- return result;
- }
-
- private async Task<MetadataResult<Episode>> GetEpisode(EpisodeInfo searchInfo, CancellationToken cancellationToken)
- {
- var result = new MetadataResult<Episode>
- {
- QueriedById = true
- };
-
- string seriesTvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb);
- string episodeTvdbId = null;
- try
- {
- episodeTvdbId = await _tvdbClientManager
- .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
- if (string.IsNullOrEmpty(episodeTvdbId))
- {
- _logger.LogError(
- "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
- searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId);
- return result;
- }
-
- var episodeResult = await _tvdbClientManager.GetEpisodesAsync(
- Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage,
- cancellationToken).ConfigureAwait(false);
-
- result = MapEpisodeToResult(searchInfo, episodeResult.Data);
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Failed to retrieve episode with id {EpisodeTvDbId}, series id {SeriesTvdbId}", episodeTvdbId, seriesTvdbId);
- }
-
- return result;
- }
-
- private static MetadataResult<Episode> MapEpisodeToResult(EpisodeInfo id, EpisodeRecord episode)
- {
- var result = new MetadataResult<Episode>
- {
- HasMetadata = true,
- Item = new Episode
- {
- IndexNumber = id.IndexNumber,
- ParentIndexNumber = id.ParentIndexNumber,
- IndexNumberEnd = id.IndexNumberEnd,
- AirsBeforeEpisodeNumber = episode.AirsBeforeEpisode,
- AirsAfterSeasonNumber = episode.AirsAfterSeason,
- AirsBeforeSeasonNumber = episode.AirsBeforeSeason,
- Name = episode.EpisodeName,
- Overview = episode.Overview,
- CommunityRating = (float?)episode.SiteRating,
- OfficialRating = episode.ContentRating,
- }
- };
- result.ResetPeople();
-
- var item = result.Item;
- item.SetProviderId(MetadataProvider.Tvdb, episode.Id.ToString());
- item.SetProviderId(MetadataProvider.Imdb, episode.ImdbId);
-
- if (string.Equals(id.SeriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase))
- {
- item.IndexNumber = Convert.ToInt32(episode.DvdEpisodeNumber ?? episode.AiredEpisodeNumber);
- item.ParentIndexNumber = episode.DvdSeason ?? episode.AiredSeason;
- }
- else if (string.Equals(id.SeriesDisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase))
- {
- if (episode.AbsoluteNumber.GetValueOrDefault() != 0)
- {
- item.IndexNumber = episode.AbsoluteNumber;
- }
- }
- else if (episode.AiredEpisodeNumber.HasValue)
- {
- item.IndexNumber = episode.AiredEpisodeNumber;
- }
- else if (episode.AiredSeason.HasValue)
- {
- item.ParentIndexNumber = episode.AiredSeason;
- }
-
- if (DateTime.TryParse(episode.FirstAired, out var date))
- {
- // dates from tvdb are UTC but without offset or Z
- item.PremiereDate = date;
- item.ProductionYear = date.Year;
- }
-
- foreach (var director in episode.Directors)
- {
- result.AddPerson(new PersonInfo
- {
- Name = director,
- Type = PersonType.Director
- });
- }
-
- // GuestStars is a weird list of names and roles
- // Example:
- // 1: Some Actor (Role1
- // 2: Role2
- // 3: Role3)
- // 4: Another Actor (Role1
- // ...
- for (var i = 0; i < episode.GuestStars.Length; ++i)
- {
- var currentActor = episode.GuestStars[i];
- var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal);
-
- if (roleStartIndex == -1)
- {
- result.AddPerson(new PersonInfo
- {
- Type = PersonType.GuestStar,
- Name = currentActor,
- Role = string.Empty
- });
- continue;
- }
-
- var roles = new List<string> { currentActor.Substring(roleStartIndex + 1) };
-
- // Fetch all roles
- for (var j = i + 1; j < episode.GuestStars.Length; ++j)
- {
- var currentRole = episode.GuestStars[j];
- var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal);
-
- if (roleEndIndex == -1)
- {
- roles.Add(currentRole);
- continue;
- }
-
- roles.Add(currentRole.TrimEnd(')'));
- // Update the outer index (keep in mind it adds 1 after the iteration)
- i = j;
- break;
- }
-
- result.AddPerson(new PersonInfo
- {
- Type = PersonType.GuestStar,
- Name = currentActor.Substring(0, roleStartIndex).Trim(),
- Role = string.Join(", ", roles)
- });
- }
-
- foreach (var writer in episode.Writers)
- {
- result.AddPerson(new PersonInfo
- {
- Name = writer,
- Type = PersonType.Writer
- });
- }
-
- result.ResultLanguage = episode.Language.EpisodeName;
- return result;
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
-
- public int Order => 0;
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
deleted file mode 100644
index a5cd425f6..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using Microsoft.Extensions.Logging;
-using TvDbSharper;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder
- {
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<TvdbPersonImageProvider> _logger;
- private readonly ILibraryManager _libraryManager;
- private readonly TvdbClientManager _tvdbClientManager;
-
- public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager)
- {
- _libraryManager = libraryManager;
- _httpClientFactory = httpClientFactory;
- _logger = logger;
- _tvdbClientManager = tvdbClientManager;
- }
-
- /// <inheritdoc />
- public string Name => "TheTVDB";
-
- /// <inheritdoc />
- public int Order => 1;
-
- /// <inheritdoc />
- public bool Supports(BaseItem item) => item is Person;
-
- /// <inheritdoc />
- public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
- {
- yield return ImageType.Primary;
- }
-
- /// <inheritdoc />
- public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
- {
- var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { nameof(Series) },
- PersonIds = new[] { item.Id },
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- }).Cast<Series>()
- .Where(i => TvdbSeriesProvider.IsValidSeries(i.ProviderIds))
- .ToList();
-
- var infos = (await Task.WhenAll(seriesWithPerson.Select(async i =>
- await GetImageFromSeriesData(i, item.Name, cancellationToken).ConfigureAwait(false)))
- .ConfigureAwait(false))
- .Where(i => i != null)
- .Take(1);
-
- return infos;
- }
-
- private async Task<RemoteImageInfo> GetImageFromSeriesData(Series series, string personName, CancellationToken cancellationToken)
- {
- var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProvider.Tvdb));
-
- try
- {
- var actorsResult = await _tvdbClientManager
- .GetActorsAsync(tvdbId, series.GetPreferredMetadataLanguage(), cancellationToken)
- .ConfigureAwait(false);
- var actor = actorsResult.Data.FirstOrDefault(a =>
- string.Equals(a.Name, personName, StringComparison.OrdinalIgnoreCase) &&
- !string.IsNullOrEmpty(a.Image));
- if (actor == null)
- {
- return null;
- }
-
- return new RemoteImageInfo
- {
- Url = TvdbUtils.BannerUrl + actor.Image,
- Type = ImageType.Primary,
- ProviderName = Name
- };
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Failed to retrieve actor {ActorName} from series {SeriesTvdbId}", personName, tvdbId);
- return null;
- }
- }
-
- /// <inheritdoc />
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
deleted file mode 100644
index 49576d488..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
+++ /dev/null
@@ -1,155 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using Microsoft.Extensions.Logging;
-using TvDbSharper;
-using TvDbSharper.Dto;
-using RatingType = MediaBrowser.Model.Dto.RatingType;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
- {
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<TvdbSeasonImageProvider> _logger;
- private readonly TvdbClientManager _tvdbClientManager;
-
- public TvdbSeasonImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager)
- {
- _httpClientFactory = httpClientFactory;
- _logger = logger;
- _tvdbClientManager = tvdbClientManager;
- }
-
- public string Name => ProviderName;
-
- public static string ProviderName => "TheTVDB";
-
- public bool Supports(BaseItem item)
- {
- return item is Season;
- }
-
- public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
- {
- return new List<ImageType>
- {
- ImageType.Primary,
- ImageType.Banner,
- ImageType.Backdrop
- };
- }
-
- public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
- {
- var season = (Season)item;
- var series = season.Series;
-
- if (series == null || !season.IndexNumber.HasValue || !TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
- {
- return Array.Empty<RemoteImageInfo>();
- }
-
- var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProvider.Tvdb));
- var seasonNumber = season.IndexNumber.Value;
- var language = item.GetPreferredMetadataLanguage();
- var remoteImages = new List<RemoteImageInfo>();
-
- var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false);
- await foreach (var keyType in keyTypes)
- {
- var imageQuery = new ImagesQuery
- {
- KeyType = keyType,
- SubKey = seasonNumber.ToString()
- };
- try
- {
- var imageResults = await _tvdbClientManager
- .GetImagesAsync(tvdbId, imageQuery, language, cancellationToken).ConfigureAwait(false);
- remoteImages.AddRange(GetImages(imageResults.Data, language));
- }
- catch (TvDbServerException)
- {
- _logger.LogDebug("No images of type {KeyType} found for series {TvdbId}", keyType, tvdbId);
- }
- }
-
- return remoteImages;
- }
-
- private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage)
- {
- var list = new List<RemoteImageInfo>();
- // any languages with null ids are ignored
- var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data.Where(x => x.Id.HasValue);
- foreach (Image image in images)
- {
- var imageInfo = new RemoteImageInfo
- {
- RatingType = RatingType.Score,
- CommunityRating = (double?)image.RatingsInfo.Average,
- VoteCount = image.RatingsInfo.Count,
- Url = TvdbUtils.BannerUrl + image.FileName,
- ProviderName = ProviderName,
- Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation,
- ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail
- };
-
- var resolution = image.Resolution.Split('x');
- if (resolution.Length == 2)
- {
- imageInfo.Width = Convert.ToInt32(resolution[0]);
- imageInfo.Height = Convert.ToInt32(resolution[1]);
- }
-
- imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType);
- list.Add(imageInfo);
- }
-
- var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase);
- return list.OrderByDescending(i =>
- {
- if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
-
- if (string.IsNullOrEmpty(i.Language))
- {
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
-
- 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/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
deleted file mode 100644
index d96840e51..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
+++ /dev/null
@@ -1,153 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using Microsoft.Extensions.Logging;
-using TvDbSharper;
-using TvDbSharper.Dto;
-using RatingType = MediaBrowser.Model.Dto.RatingType;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
- {
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<TvdbSeriesImageProvider> _logger;
- private readonly TvdbClientManager _tvdbClientManager;
-
- public TvdbSeriesImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager)
- {
- _httpClientFactory = httpClientFactory;
- _logger = logger;
- _tvdbClientManager = tvdbClientManager;
- }
-
- public string Name => ProviderName;
-
- public static string ProviderName => "TheTVDB";
-
- public bool Supports(BaseItem item)
- {
- return item is Series;
- }
-
- public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
- {
- return new List<ImageType>
- {
- ImageType.Primary,
- ImageType.Banner,
- ImageType.Backdrop
- };
- }
-
- public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
- {
- if (!TvdbSeriesProvider.IsValidSeries(item.ProviderIds))
- {
- return Array.Empty<RemoteImageInfo>();
- }
-
- var language = item.GetPreferredMetadataLanguage();
- var remoteImages = new List<RemoteImageInfo>();
- var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
- var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken)
- .ConfigureAwait(false);
- await foreach (KeyType keyType in allowedKeyTypes)
- {
- var imageQuery = new ImagesQuery
- {
- KeyType = keyType
- };
- try
- {
- var imageResults =
- await _tvdbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken)
- .ConfigureAwait(false);
-
- remoteImages.AddRange(GetImages(imageResults.Data, language));
- }
- catch (TvDbServerException)
- {
- _logger.LogDebug("No images of type {KeyType} exist for series {TvDbId}", keyType,
- tvdbId);
- }
- }
-
- return remoteImages;
- }
-
- private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage)
- {
- var list = new List<RemoteImageInfo>();
- var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data;
-
- foreach (Image image in images)
- {
- var imageInfo = new RemoteImageInfo
- {
- RatingType = RatingType.Score,
- CommunityRating = (double?)image.RatingsInfo.Average,
- VoteCount = image.RatingsInfo.Count,
- Url = TvdbUtils.BannerUrl + image.FileName,
- ProviderName = Name,
- Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation,
- ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail
- };
-
- var resolution = image.Resolution.Split('x');
- if (resolution.Length == 2)
- {
- imageInfo.Width = Convert.ToInt32(resolution[0]);
- imageInfo.Height = Convert.ToInt32(resolution[1]);
- }
-
- imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType);
- list.Add(imageInfo);
- }
-
- var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase);
- return list.OrderByDescending(i =>
- {
- if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
-
- if (string.IsNullOrEmpty(i.Language))
- {
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
-
- 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/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
deleted file mode 100644
index e5a3e9a6a..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
+++ /dev/null
@@ -1,419 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Providers;
-using Microsoft.Extensions.Logging;
-using TvDbSharper;
-using TvDbSharper.Dto;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
- {
- internal static TvdbSeriesProvider Current { get; private set; }
-
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILogger<TvdbSeriesProvider> _logger;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localizationManager;
- private readonly TvdbClientManager _tvdbClientManager;
-
- public TvdbSeriesProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager)
- {
- _httpClientFactory = httpClientFactory;
- _logger = logger;
- _libraryManager = libraryManager;
- _localizationManager = localizationManager;
- Current = this;
- _tvdbClientManager = tvdbClientManager;
- }
-
- public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
- {
- if (IsValidSeries(searchInfo.ProviderIds))
- {
- var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
-
- if (metadata.HasMetadata)
- {
- return new List<RemoteSearchResult>
- {
- new RemoteSearchResult
- {
- Name = metadata.Item.Name,
- PremiereDate = metadata.Item.PremiereDate,
- ProductionYear = metadata.Item.ProductionYear,
- ProviderIds = metadata.Item.ProviderIds,
- SearchProviderName = Name
- }
- };
- }
- }
-
- return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
- }
-
- public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken)
- {
- var result = new MetadataResult<Series>
- {
- QueriedById = true
- };
-
- if (!IsValidSeries(itemId.ProviderIds))
- {
- result.QueriedById = false;
- await Identify(itemId).ConfigureAwait(false);
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- if (IsValidSeries(itemId.ProviderIds))
- {
- result.Item = new Series();
- result.HasMetadata = true;
-
- await FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken)
- .ConfigureAwait(false);
- }
-
- return result;
- }
-
- private async Task FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken)
- {
- var series = result.Item;
-
- if (seriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var tvdbId) && !string.IsNullOrEmpty(tvdbId))
- {
- series.SetProviderId(MetadataProvider.Tvdb, tvdbId);
- }
-
- if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out var imdbId) && !string.IsNullOrEmpty(imdbId))
- {
- series.SetProviderId(MetadataProvider.Imdb, imdbId);
- tvdbId = await GetSeriesByRemoteId(imdbId, MetadataProvider.Imdb.ToString(), metadataLanguage,
- cancellationToken).ConfigureAwait(false);
- }
-
- if (seriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out var zap2It) && !string.IsNullOrEmpty(zap2It))
- {
- series.SetProviderId(MetadataProvider.Zap2It, zap2It);
- tvdbId = await GetSeriesByRemoteId(zap2It, MetadataProvider.Zap2It.ToString(), metadataLanguage,
- cancellationToken).ConfigureAwait(false);
- }
-
- try
- {
- var seriesResult =
- await _tvdbClientManager
- .GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken)
- .ConfigureAwait(false);
- await MapSeriesToResult(result, seriesResult.Data, metadataLanguage).ConfigureAwait(false);
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Failed to retrieve series with id {TvdbId}", tvdbId);
- return;
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- result.ResetPeople();
-
- try
- {
- var actorsResult = await _tvdbClientManager
- .GetActorsAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken).ConfigureAwait(false);
- MapActorsToResult(result, actorsResult.Data);
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Failed to retrieve actors for series {TvdbId}", tvdbId);
- }
- }
-
- private async Task<string> GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken)
- {
- TvDbResponse<SeriesSearchResult[]> result = null;
-
- try
- {
- if (string.Equals(idType, MetadataProvider.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase))
- {
- result = await _tvdbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken)
- .ConfigureAwait(false);
- }
- else
- {
- result = await _tvdbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken)
- .ConfigureAwait(false);
- }
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Failed to retrieve series with remote id {RemoteId}", id);
- }
-
- return result?.Data[0].Id.ToString(CultureInfo.InvariantCulture);
- }
-
- /// <summary>
- /// Check whether a dictionary of provider IDs includes an entry for a valid TV metadata provider.
- /// </summary>
- /// <param name="seriesProviderIds">The dictionary to check.</param>
- /// <returns>True, if the dictionary contains a valid TV provider ID, otherwise false.</returns>
- internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
- {
- return seriesProviderIds.ContainsKey(MetadataProvider.Tvdb.ToString()) ||
- seriesProviderIds.ContainsKey(MetadataProvider.Imdb.ToString()) ||
- seriesProviderIds.ContainsKey(MetadataProvider.Zap2It.ToString());
- }
-
- /// <summary>
- /// Finds the series.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <param name="year">The year.</param>
- /// <param name="language">The language.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{System.String}.</returns>
- private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken)
- {
- var results = await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false);
-
- if (results.Count == 0)
- {
- var parsedName = _libraryManager.ParseName(name);
- var nameWithoutYear = parsedName.Name;
-
- if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase))
- {
- results = await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false);
- }
- }
-
- return results.Where(i =>
- {
- if (year.HasValue && i.ProductionYear.HasValue)
- {
- // Allow one year tolerance
- return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
- }
-
- return true;
- });
- }
-
- private async Task<List<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken)
- {
- var comparableName = GetComparableName(name);
- var list = new List<Tuple<List<string>, RemoteSearchResult>>();
- TvDbResponse<SeriesSearchResult[]> result;
- try
- {
- result = await _tvdbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken)
- .ConfigureAwait(false);
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "No series results found for {Name}", comparableName);
- return new List<RemoteSearchResult>();
- }
-
- foreach (var seriesSearchResult in result.Data)
- {
- var tvdbTitles = new List<string>
- {
- GetComparableName(seriesSearchResult.SeriesName)
- };
- tvdbTitles.AddRange(seriesSearchResult.Aliases.Select(GetComparableName));
-
- DateTime.TryParse(seriesSearchResult.FirstAired, out var firstAired);
- var remoteSearchResult = new RemoteSearchResult
- {
- Name = tvdbTitles.FirstOrDefault(),
- ProductionYear = firstAired.Year,
- SearchProviderName = Name
- };
-
- if (!string.IsNullOrEmpty(seriesSearchResult.Banner))
- {
- // Results from their Search endpoints already include the /banners/ part in the url, because reasons...
- remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner;
- }
-
- try
- {
- var seriesSesult =
- await _tvdbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken)
- .ConfigureAwait(false);
- remoteSearchResult.SetProviderId(MetadataProvider.Imdb, seriesSesult.Data.ImdbId);
- remoteSearchResult.SetProviderId(MetadataProvider.Zap2It, seriesSesult.Data.Zap2itId);
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Unable to retrieve series with id {TvdbId}", seriesSearchResult.Id);
- }
-
- remoteSearchResult.SetProviderId(MetadataProvider.Tvdb, seriesSearchResult.Id.ToString());
- list.Add(new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, remoteSearchResult));
- }
-
- return list
- .OrderBy(i => i.Item1.Contains(comparableName, StringComparer.OrdinalIgnoreCase) ? 0 : 1)
- .ThenBy(i => list.IndexOf(i))
- .Select(i => i.Item2)
- .ToList();
- }
-
- /// <summary>
- /// Gets the name of the comparable.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <returns>System.String.</returns>
- private string GetComparableName(string name)
- {
- name = name.ToLowerInvariant();
- name = name.Normalize(NormalizationForm.FormKD);
- name = name.Replace(", the", string.Empty).Replace("the ", " ").Replace(" the ", " ");
- name = name.Replace("&", " and " );
- name = Regex.Replace(name, @"[\p{Lm}\p{Mn}]", string.Empty); // Remove diacritics, etc
- name = Regex.Replace(name, @"[\W\p{Pc}]+", " "); // Replace sequences of non-word characters and _ with " "
- return name.Trim();
- }
-
- private async Task MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage)
- {
- Series series = result.Item;
- series.SetProviderId(MetadataProvider.Tvdb, tvdbSeries.Id.ToString());
- series.Name = tvdbSeries.SeriesName;
- series.Overview = (tvdbSeries.Overview ?? string.Empty).Trim();
- result.ResultLanguage = metadataLanguage;
- series.AirDays = TVUtils.GetAirDays(tvdbSeries.AirsDayOfWeek);
- series.AirTime = tvdbSeries.AirsTime;
- series.CommunityRating = (float?)tvdbSeries.SiteRating;
- series.SetProviderId(MetadataProvider.Imdb, tvdbSeries.ImdbId);
- series.SetProviderId(MetadataProvider.Zap2It, tvdbSeries.Zap2itId);
- if (Enum.TryParse(tvdbSeries.Status, true, out SeriesStatus seriesStatus))
- {
- series.Status = seriesStatus;
- }
-
- if (DateTime.TryParse(tvdbSeries.FirstAired, out var date))
- {
- // dates from tvdb are UTC but without offset or Z
- series.PremiereDate = date;
- series.ProductionYear = date.Year;
- }
-
- if (!string.IsNullOrEmpty(tvdbSeries.Runtime) && double.TryParse(tvdbSeries.Runtime, out double runtime))
- {
- series.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
- }
-
- foreach (var genre in tvdbSeries.Genre)
- {
- series.AddGenre(genre);
- }
-
- if (!string.IsNullOrEmpty(tvdbSeries.Network))
- {
- series.AddStudio(tvdbSeries.Network);
- }
-
- if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended)
- {
- try
- {
- var episodeSummary = await _tvdbClientManager.GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).ConfigureAwait(false);
-
- if (episodeSummary.Data.AiredSeasons.Length != 0)
- {
- var maxSeasonNumber = episodeSummary.Data.AiredSeasons.Max(s => Convert.ToInt32(s, CultureInfo.InvariantCulture));
- var episodeQuery = new EpisodeQuery
- {
- AiredSeason = maxSeasonNumber
- };
- var episodesPage = await _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).ConfigureAwait(false);
-
- result.Item.EndDate = episodesPage.Data
- .Select(e => DateTime.TryParse(e.FirstAired, out var firstAired) ? firstAired : (DateTime?)null)
- .Max();
- }
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Failed to find series end date for series {TvdbId}", tvdbSeries.Id);
- }
- }
- }
-
- private static void MapActorsToResult(MetadataResult<Series> result, IEnumerable<Actor> actors)
- {
- foreach (Actor actor in actors)
- {
- var personInfo = new PersonInfo
- {
- Type = PersonType.Actor,
- Name = (actor.Name ?? string.Empty).Trim(),
- Role = actor.Role,
- SortOrder = actor.SortOrder
- };
-
- if (!string.IsNullOrEmpty(actor.Image))
- {
- personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image;
- }
-
- if (!string.IsNullOrWhiteSpace(personInfo.Name))
- {
- result.AddPerson(personInfo);
- }
- }
- }
-
- public string Name => "TheTVDB";
-
- public async Task Identify(SeriesInfo info)
- {
- if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProvider.Tvdb)))
- {
- return;
- }
-
- var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None)
- .ConfigureAwait(false);
-
- var entry = srch.FirstOrDefault();
-
- if (entry != null)
- {
- var id = entry.GetProviderId(MetadataProvider.Tvdb);
- info.SetProviderId(MetadataProvider.Tvdb, id);
- }
- }
-
- 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/TheTvdb/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
deleted file mode 100644
index 37a8d04a6..000000000
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Providers.Plugins.TheTvdb
-{
- public static class TvdbUtils
- {
- public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
- public const string TvdbBaseUrl = "https://www.thetvdb.com/";
- public const string TvdbImageBaseUrl = "https://www.thetvdb.com";
- public const string BannerUrl = TvdbImageBaseUrl + "/banners/";
-
- public static ImageType GetImageTypeFromKeyType(string keyType)
- {
- switch (keyType.ToLowerInvariant())
- {
- case "poster":
- case "season": return ImageType.Primary;
- case "series":
- case "seasonwide": return ImageType.Banner;
- case "fanart": return ImageType.Backdrop;
- default: throw new ArgumentException($"Invalid or unknown keytype: {keyType}", nameof(keyType));
- }
- }
-
- public static string NormalizeLanguage(string language)
- {
- if (string.IsNullOrWhiteSpace(language))
- {
- return null;
- }
-
- // pt-br is just pt to tvdb
- return language.Split('-')[0].ToLowerInvariant();
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index b754a0795..0e8a5baab 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -98,7 +98,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
if (preferredLanguage.Length == 5) // like en-US
{
- // Currenty, TMDB supports 2-letter language codes only
+ // Currently, TMDB supports 2-letter language codes only
// They are planning to change this in the future, thus we're
// supplying both codes if we're having a 5-letter code.
languages.Add(preferredLanguage.Substring(0, 2));
diff --git a/MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs b/MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs
deleted file mode 100644
index 40c5f2d78..000000000
--- a/MediaBrowser.Providers/TV/TvdbEpisodeExternalId.cs
+++ /dev/null
@@ -1,28 +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 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
deleted file mode 100644
index 4c54de9f8..000000000
--- a/MediaBrowser.Providers/TV/TvdbExternalId.cs
+++ /dev/null
@@ -1,28 +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 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
deleted file mode 100644
index 807ebb3ee..000000000
--- a/MediaBrowser.Providers/TV/TvdbSeasonExternalId.cs
+++ /dev/null
@@ -1,28 +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 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
index c9f314af9..3cb18e424 100644
--- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
@@ -4,7 +4,6 @@ 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
{
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 45fd9add9..87d1e9464 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -15,7 +15,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index 9cc0344c1..bce4cf009 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -134,7 +134,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- // int.TryParse is local aware, so it can be probamatic, force us culture
+ // int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
{
item.AirsBeforeEpisodeNumber = rval;
@@ -150,7 +150,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- // int.TryParse is local aware, so it can be probamatic, force us culture
+ // int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
{
item.AirsAfterSeasonNumber = rval;
@@ -166,7 +166,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- // int.TryParse is local aware, so it can be probamatic, force us culture
+ // int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
{
item.AirsBeforeSeasonNumber = rval;
@@ -182,7 +182,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- // int.TryParse is local aware, so it can be probamatic, force us culture
+ // int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
{
item.AirsBeforeSeasonNumber = rval;
@@ -198,7 +198,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- // int.TryParse is local aware, so it can be probamatic, force us culture
+ // int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
{
item.AirsBeforeEpisodeNumber = rval;
diff --git a/README.md b/README.md
index 435e709b3..1ab246f84 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ These instructions will help you get set up with a local development environment
### Prerequisites
-Before the project can be built, you must first install the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) on your system.
+Before the project can be built, you must first install the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) on your system.
Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download).
@@ -142,7 +142,7 @@ A second option is to build the project and then run the resulting executable fi
```bash
dotnet build # Build the project
- cd bin/Debug/netcoreapp3.1 # Change into the build output directory
+ cd bin/Debug/net5.0 # Change into the build output directory
```
2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`.
diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs
index 745ec359c..7d6a471f9 100644
--- a/RSSDP/DisposableManagedObjectBase.cs
+++ b/RSSDP/DisposableManagedObjectBase.cs
@@ -5,7 +5,7 @@ using System.Text;
namespace Rssdp.Infrastructure
{
/// <summary>
- /// Correclty implements the <see cref="IDisposable"/> interface and pattern for an object containing only managed resources, and adds a few common niceities not on the interface such as an <see cref="IsDisposed"/> property.
+ /// Correctly implements the <see cref="IDisposable"/> interface and pattern for an object containing only managed resources, and adds a few common niceties not on the interface such as an <see cref="IsDisposed"/> property.
/// </summary>
public abstract class DisposableManagedObjectBase : IDisposable
{
@@ -61,10 +61,10 @@ namespace Rssdp.Infrastructure
/// Disposes this object instance and all internally managed resources.
/// </summary>
/// <remarks>
- /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behaviour of derived classes.</para>
+ /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behavior of derived classes.</para>
/// </remarks>
/// <seealso cref="IsDisposed"/>
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "We do exactly as asked, but CA doesn't seem to like us also setting the IsDisposed property. Too bad, it's a good idea and shouldn't cause an exception or anything likely to interfer with the dispose process.")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "We do exactly as asked, but CA doesn't seem to like us also setting the IsDisposed property. Too bad, it's a good idea and shouldn't cause an exception or anything likely to interfere with the dispose process.")]
public void Dispose()
{
IsDisposed = true;
diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs
index 11202940e..c56249523 100644
--- a/RSSDP/HttpParserBase.cs
+++ b/RSSDP/HttpParserBase.cs
@@ -105,7 +105,7 @@ namespace Rssdp.Infrastructure
var headerName = line.Substring(0, headerKeySeparatorIndex).Trim();
var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim();
- // Not sure how to determine where request headers and and content headers begin,
+ // Not sure how to determine where request headers and content headers begin,
// at least not without a known set of headers (general headers first the content headers)
// which seems like a bad way of doing it. So we'll assume if it's a known content header put it there
// else use request headers.
diff --git a/RSSDP/ISsdpDeviceLocator.cs b/RSSDP/ISsdpDeviceLocator.cs
index 413055643..4df166cd2 100644
--- a/RSSDP/ISsdpDeviceLocator.cs
+++ b/RSSDP/ISsdpDeviceLocator.cs
@@ -52,7 +52,7 @@ namespace Rssdp.Infrastructure
}
/// <summary>
- /// Aynchronously performs a search for all devices using the default search timeout, and returns an awaitable task that can be used to retrieve the results.
+ /// Asynchronously performs a search for all devices using the default search timeout, and returns an awaitable task that can be used to retrieve the results.
/// </summary>
/// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync();
@@ -83,7 +83,7 @@ namespace Rssdp.Infrastructure
/// </param>
/// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 is recommended by the UPnP 1.1 specification. Specify TimeSpan.Zero to return only devices already in the cache.</param>
/// <remarks>
- /// <para>By design RSSDP does not support 'publishing services' as it is intended for use with non-standard UPnP devices that don't publish UPnP style services. However, it is still possible to use RSSDP to search for devices implemetning these services if you know the service type.</para>
+ /// <para>By design RSSDP does not support 'publishing services' as it is intended for use with non-standard UPnP devices that don't publish UPnP style services. However, it is still possible to use RSSDP to search for devices implementing these services if you know the service type.</para>
/// </remarks>
/// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync(string searchTarget, TimeSpan searchWaitTime);
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
index 664663bd7..d0962e82c 100644
--- a/RSSDP/RSSDP.csproj
+++ b/RSSDP/RSSDP.csproj
@@ -10,7 +10,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index 1a8577d8d..90925b9e0 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -102,7 +102,7 @@ namespace Rssdp.Infrastructure
/// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
/// <exception cref="InvalidOperationException">Thrown if the <paramref name="device"/> contains property values that are not acceptable to the UPnP 1.0 specification.</exception>
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "t", Justification = "Capture task to local variable supresses compiler warning, but task is not really needed.")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "t", Justification = "Capture task to local variable suppresses compiler warning, but task is not really needed.")]
public void AddDevice(SsdpRootDevice device)
{
if (device == null)
@@ -180,7 +180,7 @@ namespace Rssdp.Infrastructure
/// </summary>
/// <remarks>
/// <para>Enabling this option will cause devices to show up in Microsoft Windows Explorer's network screens (if discovery is enabled etc.). Windows Explorer appears to search only for pnp:rootdeivce and not upnp:rootdevice.</para>
- /// <para>If false, the system will only use upnp:rootdevice for notifiation broadcasts and and search responses, which is correct according to the UPnP/SSDP spec.</para>
+ /// <para>If false, the system will only use upnp:rootdevice for notification broadcasts and and search responses, which is correct according to the UPnP/SSDP spec.</para>
/// </remarks>
public bool SupportPnpRootDevice
{
diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs
index 8937ec331..4084b31ca 100644
--- a/RSSDP/SsdpRootDevice.cs
+++ b/RSSDP/SsdpRootDevice.cs
@@ -25,7 +25,7 @@ namespace Rssdp
/// Specifies how long clients can cache this device's details for. Optional but defaults to <see cref="TimeSpan.Zero"/> which means no-caching. Recommended value is half an hour.
/// </summary>
/// <remarks>
- /// <para>Specifiy <see cref="TimeSpan.Zero"/> to indicate no caching allowed.</para>
+ /// <para>Specify <see cref="TimeSpan.Zero"/> to indicate no caching allowed.</para>
/// <para>Also used to specify how often to rebroadcast alive notifications.</para>
/// <para>The UPnP/SSDP specifications indicate this should not be less than 1800 seconds (half an hour), but this is not enforced by this library.</para>
/// </remarks>
@@ -50,7 +50,7 @@ namespace Rssdp
public IPAddress SubnetMask { get; set; }
/// <summary>
- /// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional.
+ /// The base URL to use for all relative url's provided in other properties (and those of child devices). Optional.
/// </summary>
/// <remarks>
/// <para>Defines the base URL. Used to construct fully-qualified URLs. All relative URLs that appear elsewhere in the description are combined with this base URL. If URLBase is empty or not given, the base URL is the URL from which the device description was retrieved (which is the preferred implementation; use of URLBase is no longer recommended). Specified by UPnP vendor. Single URL.</para>
diff --git a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj b/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj
index 47aeed05e..c564e86e9 100644
--- a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj
+++ b/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
diff --git a/debian/control b/debian/control
index 9216d24fe..9675d36ca 100644
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Section: misc
Priority: optional
Maintainer: Jellyfin Team <team@jellyfin.org>
Build-Depends: debhelper (>= 9),
- dotnet-sdk-3.1,
+ dotnet-sdk-5.0,
libc6-dev,
libcurl4-openssl-dev,
libfontconfig1-dev,
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 39788cc0e..01fc1aaac 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -2,7 +2,7 @@ FROM centos:7
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index aaca8fe01..f0d9188c1 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -2,7 +2,7 @@ FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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 594da04ce..8132ee887 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -2,7 +2,7 @@ FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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 3e6e2d0d7..31f534838 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -2,7 +2,7 @@ FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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.docker.amd64 b/deployment/Dockerfile.docker.amd64
index e04442606..0b1a57014 100644
--- a/deployment/Dockerfile.docker.amd64
+++ b/deployment/Dockerfile.docker.amd64
@@ -1,6 +1,6 @@
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64
index a7ac40492..583f53ca0 100644
--- a/deployment/Dockerfile.docker.arm64
+++ b/deployment/Dockerfile.docker.arm64
@@ -1,6 +1,6 @@
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf
index b5a42f55f..177c11713 100644
--- a/deployment/Dockerfile.docker.armhf
+++ b/deployment/Dockerfile.docker.armhf
@@ -1,6 +1,6 @@
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 01b99deb6..2549f25ee 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -1,8 +1,8 @@
-FROM fedora:31
+FROM fedora:33
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -10,7 +10,7 @@ ENV IS_DOCKER=YES
# Prepare Fedora environment
RUN dnf update -y \
- && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel
+ && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd
# Install DotNET SDK
RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index f98881ebf..2bedafcc5 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -2,7 +2,7 @@ FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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 ec9d2d8c7..d470f9b74 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -2,7 +2,7 @@ FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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 3523f8ace..d2007c075 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -2,7 +2,7 @@ FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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 0a365e1ae..084159d45 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -2,7 +2,7 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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 ab3ec9b9f..c2caf4cf8 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -2,7 +2,7 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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 fa41bdf48..719b3a85b 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -2,7 +2,7 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-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 7216b2363..e6905c906 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -2,7 +2,7 @@ FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=3.1
+ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -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/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
index 012e1cebf..145e28d87 100755
--- a/deployment/build.debian.amd64
+++ b/deployment/build.debian.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-3.1,/d' debian/control
+ sed -i '/dotnet-sdk-5.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64
index 12ce3e874..5699133a0 100755
--- a/deployment/build.debian.arm64
+++ b/deployment/build.debian.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-3.1,/d' debian/control
+ sed -i '/dotnet-sdk-5.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf
index 3089eab58..20af2ddfb 100755
--- a/deployment/build.debian.armhf
+++ b/deployment/build.debian.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-3.1,/d' debian/control
+ sed -i '/dotnet-sdk-5.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64
index 0eac9cdd1..0c29286c0 100755
--- a/deployment/build.ubuntu.amd64
+++ b/deployment/build.ubuntu.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-3.1,/d' debian/control
+ sed -i '/dotnet-sdk-5.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64
index 5b11fd543..65d67f80f 100755
--- a/deployment/build.ubuntu.arm64
+++ b/deployment/build.ubuntu.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-3.1,/d' debian/control
+ sed -i '/dotnet-sdk-5.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf
index 4734cf658..370370abc 100755
--- a/deployment/build.ubuntu.armhf
+++ b/deployment/build.ubuntu.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-3.1, since it's installed manually
+ # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-3.1,/d' debian/control
+ sed -i '/dotnet-sdk-5.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 93fb9fb41..13305488e 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
# Requirements not packaged in main repos
# COPR @dotnet-sig/dotnet or
# https://packages.microsoft.com/rhel/7/prod/
-BuildRequires: dotnet-runtime-3.1, dotnet-sdk-3.1
+BuildRequires: dotnet-runtime-5.0, dotnet-sdk-5.0
Requires: %{name}-server = %{version}-%{release}, %{name}-web >= 10.6, %{name}-web < 10.7
# Disable Automatic Dependency Processing
AutoReqProv: no
@@ -82,7 +82,6 @@ EOF
%{_libdir}/jellyfin/*
# Needs 755 else only root can run it since binary build by dotnet is 722
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
-%{_libdir}/jellyfin/SOS_README.md
%{_unitdir}/jellyfin.service
%{_libexecdir}/jellyfin/restart.sh
%{_prefix}/lib/firewalld/services/jellyfin.xml
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index a46d94457..90c491666 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -81,7 +81,7 @@ namespace Jellyfin.Api.Tests.Auth
var authenticateResult = await _sut.AuthenticateAsync();
Assert.False(authenticateResult.Succeeded);
- Assert.Equal(errorMessage, authenticateResult.Failure.Message);
+ Assert.Equal(errorMessage, authenticateResult.Failure?.Message);
}
[Fact]
@@ -100,7 +100,7 @@ namespace Jellyfin.Api.Tests.Auth
var authorizationInfo = SetupUser();
var authenticateResult = await _sut.AuthenticateAsync();
- Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
+ Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
}
[Theory]
@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Tests.Auth
var authenticateResult = await _sut.AuthenticateAsync();
var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
- Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
+ Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Role, expectedRole));
}
[Fact]
@@ -121,7 +121,7 @@ namespace Jellyfin.Api.Tests.Auth
SetupUser();
var authenticatedResult = await _sut.AuthenticateAsync();
- Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
+ Assert.Equal(_scheme.Name, authenticatedResult.Ticket?.AuthenticationScheme);
}
private AuthorizationInfo SetupUser(bool isAdmin = false)
diff --git a/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs b/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs
index 6fc287420..1cbe94c5b 100644
--- a/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs
+++ b/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs
@@ -25,7 +25,7 @@ namespace Jellyfin.Api.Tests
// Assert
response.EnsureSuccessStatusCode();
- Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
var responseBody = await response.Content.ReadAsStreamAsync();
_ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody);
}
@@ -43,7 +43,7 @@ namespace Jellyfin.Api.Tests
// Assert
response.EnsureSuccessStatusCode();
- Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType?.ToString());
}
}
}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index ce61f5684..14eed30e0 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
@@ -16,13 +16,13 @@
<PackageReference Include="AutoFixture" Version="4.14.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
- <PackageReference Include="Moq" Version="4.14.7" />
+ <PackageReference Include="Moq" Version="4.15.1" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
index 82c7f6427..3ae6ae5bd 100644
--- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery()
{
var queryParamName = "test";
- var queryParamValues = new[] { "lol", "xd" };
+ IReadOnlyList<string> queryParamValues = new[] { "lol", "xd" };
var queryParamString = "lol,xd";
var queryParamType = typeof(string[]);
@@ -36,14 +36,14 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Equal((string[])bindingContextMock.Object.Result.Model, queryParamValues);
+ Assert.Equal((IReadOnlyList<string>?)bindingContextMock.Object?.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery()
{
var queryParamName = "test";
- var queryParamValues = new[] { 42, 0 };
+ IReadOnlyList<int> queryParamValues = new[] { 42, 0 };
var queryParamString = "42,0";
var queryParamType = typeof(int[]);
@@ -61,14 +61,14 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Equal((int[])bindingContextMock.Object.Result.Model, queryParamValues);
+ Assert.Equal((IReadOnlyList<int>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery()
{
var queryParamName = "test";
- var queryParamValues = new[] { TestType.How, TestType.Much };
+ IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
var queryParamString = "How,Much";
var queryParamType = typeof(TestType[]);
@@ -86,14 +86,14 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas()
{
var queryParamName = "test";
- var queryParamValues = new[] { TestType.How, TestType.Much };
+ IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
var queryParamString = "How,,Much";
var queryParamType = typeof(TestType[]);
@@ -111,14 +111,14 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery()
{
var queryParamName = "test";
- var queryParamValues = new[] { TestType.How, TestType.Much };
+ IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
var queryParamString1 = "How";
var queryParamString2 = "Much";
var queryParamType = typeof(TestType[]);
@@ -141,14 +141,14 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery()
{
var queryParamName = "test";
- var queryParamValues = Array.Empty<TestType>();
+ IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>();
var queryParamType = typeof(TestType[]);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
@@ -177,7 +177,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
{
var queryParamName = "test";
var queryParamString = "🔥,😢";
- var queryParamType = typeof(TestType[]);
+ var queryParamType = typeof(IReadOnlyList<TestType>);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
@@ -192,7 +192,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Empty((TestType[])bindingContextMock.Object.Result.Model);
+ Assert.Empty((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model);
}
[Fact]
@@ -201,7 +201,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamName = "test";
var queryParamString1 = "How";
var queryParamString2 = "😱";
- var queryParamType = typeof(TestType[]);
+ var queryParamType = typeof(IReadOnlyList<TestType>);
var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
@@ -220,7 +220,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Single((TestType[])bindingContextMock.Object.Result.Model);
+ Assert.Single((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model);
}
}
}
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs
new file mode 100644
index 000000000..938d19a15
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs
@@ -0,0 +1,226 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+using Jellyfin.Api.ModelBinders;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Primitives;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.ModelBinders
+{
+ public sealed class PipeDelimitedArrayModelBinderTests
+ {
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedStringArrayQuery()
+ {
+ var queryParamName = "test";
+ IReadOnlyList<string> queryParamValues = new[] { "lol", "xd" };
+ var queryParamString = "lol|xd";
+ var queryParamType = typeof(string[]);
+
+ var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((IReadOnlyList<string>?)bindingContextMock.Object?.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidDelimitedIntArrayQuery()
+ {
+ var queryParamName = "test";
+ IReadOnlyList<int> queryParamValues = new[] { 42, 0 };
+ var queryParamString = "42|0";
+ var queryParamType = typeof(int[]);
+
+ var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((IReadOnlyList<int>?)bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
+ var queryParamString = "How|Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQueryWithDoublePipes()
+ {
+ var queryParamName = "test";
+ IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
+ var queryParamString = "How||Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
+ var queryParamString1 = "How";
+ var queryParamString2 = "Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>
+ {
+ { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>();
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>
+ {
+ { queryParamName, new StringValues(value: null) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_EnumArrayQuery_BindValidOnly()
+ {
+ var queryParamName = "test";
+ var queryParamString = "🔥|😢";
+ var queryParamType = typeof(IReadOnlyList<TestType>);
+
+ var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Empty((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2()
+ {
+ var queryParamName = "test";
+ var queryParamString1 = "How";
+ var queryParamString2 = "😱";
+ var queryParamType = typeof(IReadOnlyList<TestType>);
+
+ var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>
+ {
+ { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Single((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs
index 3a85b5514..03ab56d1f 100644
--- a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs
+++ b/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Api.Tests
// Assert
response.EnsureSuccessStatusCode();
- Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
// Write out for publishing
var responseBody = await response.Content.ReadAsStringAsync();
diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs
index c4ce39885..f27cdf7b6 100644
--- a/tests/Jellyfin.Api.Tests/TestHelpers.cs
+++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs
@@ -60,7 +60,7 @@ namespace Jellyfin.Api.Tests
.Returns(user);
httpContextAccessorMock
- .Setup(h => h.HttpContext.Connection.RemoteIpAddress)
+ .Setup(h => h.HttpContext!.Connection.RemoteIpAddress)
.Returns(new IPAddress(0));
return new ClaimsPrincipal(identity);
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 67dc8286a..e8eca6760 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 30e84842a..6e3fac43d 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 4fd0d5342..e88de3811 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index e93385136..64d51e063 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs
index a214bc57c..cf21f964e 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs
@@ -1,4 +1,4 @@
-using Emby.Naming.AudioBook;
+using Emby.Naming.AudioBook;
using Xunit;
namespace Jellyfin.Naming.Tests.AudioBook
@@ -8,22 +8,22 @@ namespace Jellyfin.Naming.Tests.AudioBook
[Fact]
public void CompareTo_Same_Success()
{
- var info = new AudioBookFileInfo();
+ var info = new AudioBookFileInfo(string.Empty, string.Empty);
Assert.Equal(0, info.CompareTo(info));
}
[Fact]
public void CompareTo_Null_Success()
{
- var info = new AudioBookFileInfo();
+ var info = new AudioBookFileInfo(string.Empty, string.Empty);
Assert.Equal(1, info.CompareTo(null));
}
[Fact]
public void CompareTo_Empty_Success()
{
- var info1 = new AudioBookFileInfo();
- var info2 = new AudioBookFileInfo();
+ var info1 = new AudioBookFileInfo(string.Empty, string.Empty);
+ var info2 = new AudioBookFileInfo(string.Empty, string.Empty);
Assert.Equal(0, info1.CompareTo(info2));
}
}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
index 1084e20bd..e5768b620 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
@@ -1,4 +1,6 @@
-using System.Linq;
+using System;
+using System.Collections.Generic;
+using System.Linq;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
@@ -18,11 +20,22 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
"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",
+ "Harry Potter and the Deathly Hallows/Extra.mp3",
"Batman/Chapter 1.mp3",
"Batman/Chapter 2.mp3",
"Batman/Chapter 3.mp3",
+
+ "Badman/audiobook.mp3",
+ "Badman/extra.mp3",
+
+ "Superman (2020)/Part 1.mp3",
+ "Superman (2020)/extra.mp3",
+
+ "Ready Player One (2020)/audiobook.mp3",
+ "Ready Player One (2020)/extra.mp3",
+
+ ".mp3"
};
var resolver = GetResolver();
@@ -33,13 +46,141 @@ namespace Jellyfin.Naming.Tests.AudioBook
FullName = i
})).ToList();
+ Assert.Equal(5, result.Count);
+
Assert.Equal(2, result[0].Files.Count);
- // Assert.Empty(result[0].Extras); FIXME: AudioBookListResolver should resolve extra files properly
+ Assert.Single(result[0].Extras);
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);
+
+ Assert.Single(result[2].Files);
+ Assert.Single(result[2].Extras);
+ Assert.Equal("Badman", result[2].Name);
+
+ Assert.Single(result[3].Files);
+ Assert.Single(result[3].Extras);
+ Assert.Equal("Superman", result[3].Name);
+
+ Assert.Single(result[4].Files);
+ Assert.Single(result[4].Extras);
+ Assert.Equal("Ready Player One", result[4].Name);
+ }
+
+ [Fact]
+ public void TestAlternativeVersions()
+ {
+ var files = new[]
+ {
+ "Harry Potter and the Deathly Hallows/Chapter 1.ogg",
+ "Harry Potter and the Deathly Hallows/Chapter 1.mp3",
+
+ "Deadpool.mp3",
+ "Deadpool [HQ].mp3",
+
+ "Superman/audiobook.mp3",
+ "Superman/Superman.mp3",
+ "Superman/Superman [HQ].mp3",
+ "Superman/extra.mp3",
+
+ "Batman/ Chapter 1 .mp3",
+ "Batman/Chapter 1[loss-less].mp3"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ })).ToList();
+
+ Assert.Equal(5, result.Count);
+ // HP - Same name so we don't care which file is alternative
+ Assert.Single(result[0].AlternateVersions);
+ // DP
+ Assert.Empty(result[1].AlternateVersions);
+ // DP HQ (directory missing so we do not group deadpools together)
+ Assert.Empty(result[2].AlternateVersions);
+ // Superman
+ // Priority:
+ // 1. Name
+ // 2. audiobook
+ // 3. Names with modifiers
+ Assert.Equal(2, result[3].AlternateVersions.Count);
+ var paths = result[3].AlternateVersions.Select(x => x.Path).ToList();
+ Assert.Contains("Superman/audiobook.mp3", paths);
+ Assert.Contains("Superman/Superman [HQ].mp3", paths);
+ // Batman
+ Assert.Single(result[4].AlternateVersions);
+ }
+
+ [Fact]
+ public void TestNameYearExtraction()
+ {
+ var data = new[]
+ {
+ new NameYearPath
+ {
+ Name = "Harry Potter and the Deathly Hallows",
+ Path = "Harry Potter and the Deathly Hallows (2007)/Chapter 1.ogg",
+ Year = 2007
+ },
+ new NameYearPath
+ {
+ Name = "Batman",
+ Path = "Batman (2020).ogg",
+ Year = 2020
+ },
+ new NameYearPath
+ {
+ Name = "Batman",
+ Path = "Batman( 2021 ).mp3",
+ Year = 2021
+ },
+ new NameYearPath
+ {
+ Name = "Batman(*2021*)",
+ Path = "Batman(*2021*).mp3",
+ Year = null
+ },
+ new NameYearPath
+ {
+ Name = "Batman",
+ Path = "Batman.mp3",
+ Year = null
+ },
+ new NameYearPath
+ {
+ Name = "+ Batman .",
+ Path = " + Batman . .mp3",
+ Year = null
+ },
+ new NameYearPath
+ {
+ Name = " ",
+ Path = " .mp3",
+ Year = null
+ }
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(data.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i.Path
+ })).ToList();
+
+ Assert.Equal(data.Length, result.Count);
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ Assert.Equal(data[i].Name, result[i].Name);
+ Assert.Equal(data[i].Year, result[i].Year);
+ }
}
[Fact]
@@ -82,9 +223,51 @@ namespace Jellyfin.Naming.Tests.AudioBook
Assert.Single(result);
}
+ [Fact]
+ public void TestWithoutFolder()
+ {
+ var files = new[]
+ {
+ "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);
+ }
+
+ [Fact]
+ public void TestEmpty()
+ {
+ var files = Array.Empty<string>();
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ })).ToList();
+
+ Assert.Empty(result);
+ }
+
private AudioBookListResolver GetResolver()
{
return new AudioBookListResolver(_namingOptions);
}
+
+ internal struct NameYearPath
+ {
+ public string Name;
+ public string Path;
+ public int? Year;
+ }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index 673289436..b3257ace3 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
@@ -14,30 +14,24 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
yield return new object[]
{
- new AudioBookFileInfo()
- {
- Path = @"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
- Container = "mp3",
- }
+ new AudioBookFileInfo(
+ @"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
+ "mp3")
};
yield return new object[]
{
- new AudioBookFileInfo()
- {
- Path = @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
- Container = "ogg",
- ChapterNumber = 1
- }
+ new AudioBookFileInfo(
+ @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
+ "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
- }
+ new AudioBookFileInfo(
+ @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
+ "mp3",
+ chapterNumber: 2,
+ partNumber: 3)
};
}
@@ -52,13 +46,22 @@ namespace Jellyfin.Naming.Tests.AudioBook
Assert.Equal(result!.Container, expectedResult.Container);
Assert.Equal(result!.ChapterNumber, expectedResult.ChapterNumber);
Assert.Equal(result!.PartNumber, expectedResult.PartNumber);
- Assert.Equal(result!.IsDirectory, expectedResult.IsDirectory);
}
[Fact]
- public void Resolve_EmptyFileName_ArgumentException()
+ public void Resolve_InvalidExtension()
{
- Assert.Throws<ArgumentException>(() => new AudioBookResolver(_namingOptions).Resolve(string.Empty));
+ var result = new AudioBookResolver(_namingOptions).Resolve(@"/server/AudioBooks/Larry Potter/Larry Potter.mp9");
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void Resolve_EmptyFileName()
+ {
+ var result = new AudioBookResolver(_namingOptions).Resolve(string.Empty);
+
+ Assert.Null(result);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
new file mode 100644
index 000000000..3892d00f6
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
@@ -0,0 +1,36 @@
+using Emby.Naming.Common;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.Common
+{
+ public class NamingOptionsTest
+ {
+ [Fact]
+ public void TestNamingOptionsCompile()
+ {
+ var options = new NamingOptions();
+
+ Assert.NotEmpty(options.VideoFileStackingRegexes);
+ Assert.NotEmpty(options.CleanDateTimeRegexes);
+ Assert.NotEmpty(options.CleanStringRegexes);
+ Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes);
+ Assert.NotEmpty(options.EpisodeMultiPartRegexes);
+ }
+
+ [Fact]
+ public void TestNamingOptionsEpisodeExpressions()
+ {
+ var exp = new EpisodeExpression(string.Empty);
+
+ Assert.False(exp.IsOptimistic);
+ exp.IsOptimistic = true;
+ Assert.True(exp.IsOptimistic);
+
+ Assert.Equal(string.Empty, exp.Expression);
+ Assert.NotNull(exp.Regex);
+ exp.Expression = "test";
+ Assert.Equal("test", exp.Expression);
+ Assert.NotNull(exp.Regex);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 0d240fd65..567cf34ef 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
index d11809de1..f3abacb4f 100644
--- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Emby.Naming.Common;
using Emby.Naming.Subtitles;
using Xunit;
@@ -26,21 +26,17 @@ namespace Jellyfin.Naming.Tests.Subtitles
Assert.Equal(language, result?.Language, true);
Assert.Equal(isDefault, result?.IsDefault);
Assert.Equal(isForced, result?.IsForced);
+ Assert.Equal(input, result?.Path);
}
[Theory]
[InlineData("The Skin I Live In (2011).mp4")]
+ [InlineData("")]
public void SubtitleParser_InvalidFileName_ReturnsNull(string input)
{
var parser = new SubtitleParser(_namingOptions);
Assert.Null(parser.ParseFile(input));
}
-
- [Fact]
- public void SubtitleParser_EmptyFileName_ThrowsArgumentException()
- {
- Assert.Throws<ArgumentException>(() => new SubtitleParser(_namingOptions).ParseFile(string.Empty));
- }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
index 03aeb7f76..12fc19bc4 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
@@ -1,4 +1,4 @@
-using Emby.Naming.Common;
+using Emby.Naming.Common;
using Emby.Naming.TV;
using Xunit;
@@ -7,43 +7,98 @@ namespace Jellyfin.Naming.Tests.TV
public class EpisodePathParserTest
{
[Theory]
- [InlineData("/media/Foo/Foo-S01E01", "Foo", 1, 1)]
- [InlineData("/media/Foo - S04E011", "Foo", 4, 11)]
- [InlineData("/media/Foo/Foo s01x01", "Foo", 1, 1)]
- [InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", "Foo (2019)", 4, 3)]
- [InlineData("D:\\media\\Foo\\Foo-S01E01", "Foo", 1, 1)]
- [InlineData("D:\\media\\Foo - S04E011", "Foo", 4, 11)]
- [InlineData("D:\\media\\Foo\\Foo s01x01", "Foo", 1, 1)]
- [InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", "Foo (2019)", 4, 3)]
- [InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", "Elementary", 2, 3)]
- [InlineData("/Season 1/seriesname S01E02 blah.avi", "seriesname", 1, 2)]
- [InlineData("/Running Man/Running Man S2017E368.mkv", "Running Man", 2017, 368)]
- [InlineData("/Season 1/seriesname 01x02 blah.avi", "seriesname", 1, 2)]
- [InlineData("/Season 25/The Simpsons.S25E09.Steal this episode.mp4", "The Simpsons", 25, 9)]
- [InlineData("/Season 1/seriesname S01x02 blah.avi", "seriesname", 1, 2)]
- [InlineData("/Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", "Elementary", 2, 3)]
- [InlineData("/Season 1/seriesname S01xE02 blah.avi", "seriesname", 1, 2)]
- [InlineData("/Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", "Elementary", 2, 3)]
- [InlineData("/Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", "Elementary", 2, 3)]
- [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", "Elementary", 2, 3)]
- [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", "Elementary", 1, 23)]
- [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", "The Wonder Years", 4, 7)]
+ [InlineData("/media/Foo/Foo-S01E01", true, "Foo", 1, 1)]
+ [InlineData("/media/Foo - S04E011", true, "Foo", 4, 11)]
+ [InlineData("/media/Foo/Foo s01x01", true, "Foo", 1, 1)]
+ [InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", true, "Foo (2019)", 4, 3)]
+ [InlineData("D:\\media\\Foo\\Foo-S01E01", true, "Foo", 1, 1)]
+ [InlineData("D:\\media\\Foo - S04E011", true, "Foo", 4, 11)]
+ [InlineData("D:\\media\\Foo\\Foo s01x01", true, "Foo", 1, 1)]
+ [InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", true, "Foo (2019)", 4, 3)]
+ [InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", false, "Elementary", 2, 3)]
+ [InlineData("/Season 1/seriesname S01E02 blah.avi", false, "seriesname", 1, 2)]
+ [InlineData("/Running Man/Running Man S2017E368.mkv", false, "Running Man", 2017, 368)]
+ [InlineData("/Season 1/seriesname 01x02 blah.avi", false, "seriesname", 1, 2)]
+ [InlineData("/Season 25/The Simpsons.S25E09.Steal this episode.mp4", false, "The Simpsons", 25, 9)]
+ [InlineData("/Season 1/seriesname S01x02 blah.avi", false, "seriesname", 1, 2)]
+ [InlineData("/Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", false, "Elementary", 2, 3)]
+ [InlineData("/Season 1/seriesname S01xE02 blah.avi", false, "seriesname", 1, 2)]
+ [InlineData("/Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", false, "Elementary", 2, 3)]
+ [InlineData("/Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", false, "Elementary", 2, 3)]
+ [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)]
+ [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)]
+ [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)]
// TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)]
// TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)]
// TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)]
// TODO: [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", "The Daily Show", 25, 22)]
// TODO: [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", "Watchmen (2019)", 1, 3)]
// TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", "The Legend of Condor Heroes 2017", 1, 7)]
- public void ParseEpisodesCorrectly(string path, string name, int season, int episode)
+ public void ParseEpisodesCorrectly(string path, bool isDirectory, string name, int season, int episode)
{
NamingOptions o = new NamingOptions();
EpisodePathParser p = new EpisodePathParser(o);
- var res = p.Parse(path, false);
+ var res = p.Parse(path, isDirectory);
Assert.True(res.Success);
Assert.Equal(name, res.SeriesName);
Assert.Equal(season, res.SeasonNumber);
Assert.Equal(episode, res.EpisodeNumber);
}
+
+ [Theory]
+ [InlineData("/test/01-03.avi", true, true)]
+ public void EpisodePathParserTest_DifferentExpressionsParameters(string path, bool? isNamed, bool? isOptimistic)
+ {
+ NamingOptions o = new NamingOptions();
+ EpisodePathParser p = new EpisodePathParser(o);
+ var res = p.Parse(path, false, isNamed, isOptimistic);
+
+ Assert.True(res.Success);
+ }
+
+ [Fact]
+ public void EpisodePathParserTest_FalsePositivePixelRate()
+ {
+ NamingOptions o = new NamingOptions();
+ EpisodePathParser p = new EpisodePathParser(o);
+ var res = p.Parse("Series Special (1920x1080).mkv", false);
+
+ Assert.False(res.Success);
+ }
+
+ [Fact]
+ public void EpisodeResolverTest_WrongExtension()
+ {
+ var res = new EpisodeResolver(new NamingOptions()).Resolve("test.mp3", false);
+ Assert.Null(res);
+ }
+
+ [Fact]
+ public void EpisodeResolverTest_WrongExtensionStub()
+ {
+ var res = new EpisodeResolver(new NamingOptions()).Resolve("dvd.disc", false);
+ Assert.NotNull(res);
+ Assert.True(res!.IsStub);
+ }
+
+ /*
+ * EpisodePathParser.cs:130 is currently unreachable, but the piece of code is useful and could be reached with addition of new EpisodeExpressions.
+ * In order to preserve it but achieve 100% code coverage the test case below with made up expressions and filename is used.
+ */
+ [Fact]
+ public void EpisodePathParserTest_EmptyDateParsers()
+ {
+ NamingOptions o = new NamingOptions()
+ {
+ EpisodeExpressions = new[] { new EpisodeExpression("(([0-9]{4})-([0-9]{2})-([0-9]{2}) [0-9]{2}:[0-9]{2}:[0-9]{2})", true) }
+ };
+ o.Compile();
+
+ EpisodePathParser p = new EpisodePathParser(o);
+ var res = p.Parse("ABC_2019_10_21 11:00:00", false);
+
+ Assert.True(res.Success);
+ }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
index 3513050b6..58ea0bec5 100644
--- a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
@@ -74,7 +74,7 @@ namespace Jellyfin.Naming.Tests.TV
var result = new EpisodePathParser(options)
.Parse(filename, false);
- Assert.Equal(result.EndingEpsiodeNumber, endingEpisodeNumber);
+ Assert.Equal(result.EndingEpisodeNumber, endingEpisodeNumber);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
index 078f940b2..b7b5b54ec 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
@@ -1,4 +1,4 @@
-using Emby.Naming.TV;
+using Emby.Naming.TV;
using Xunit;
namespace Jellyfin.Naming.Tests.TV
@@ -6,26 +6,30 @@ namespace Jellyfin.Naming.Tests.TV
public class SeasonFolderTests
{
[Theory]
- [InlineData(@"/Drive/Season 1", 1)]
- [InlineData(@"/Drive/Season 2", 2)]
- [InlineData(@"/Drive/Season 02", 2)]
- [InlineData(@"/Drive/Seinfeld/S02", 2)]
- [InlineData(@"/Drive/Seinfeld/2", 2)]
- [InlineData(@"/Drive/Season 2009", 2009)]
- [InlineData(@"/Drive/Season1", 1)]
- [InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4)]
- [InlineData(@"/Drive/Season 7 (2016)", 7)]
- [InlineData(@"/Drive/Staffel 7 (2016)", 7)]
- [InlineData(@"/Drive/Stagione 7 (2016)", 7)]
- [InlineData(@"/Drive/Season (8)", null)]
- [InlineData(@"/Drive/3.Staffel", 3)]
- [InlineData(@"/Drive/s06e05", null)]
- [InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null)]
- public void GetSeasonNumberFromPathTest(string path, int? seasonNumber)
+ [InlineData(@"/Drive/Season 1", 1, true)]
+ [InlineData(@"/Drive/Season 2", 2, true)]
+ [InlineData(@"/Drive/Season 02", 2, true)]
+ [InlineData(@"/Drive/Seinfeld/S02", 2, true)]
+ [InlineData(@"/Drive/Seinfeld/2", 2, true)]
+ [InlineData(@"/Drive/Season 2009", 2009, true)]
+ [InlineData(@"/Drive/Season1", 1, true)]
+ [InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
+ [InlineData(@"/Drive/Season 7 (2016)", 7, false)]
+ [InlineData(@"/Drive/Staffel 7 (2016)", 7, false)]
+ [InlineData(@"/Drive/Stagione 7 (2016)", 7, false)]
+ [InlineData(@"/Drive/Season (8)", null, false)]
+ [InlineData(@"/Drive/3.Staffel", 3, false)]
+ [InlineData(@"/Drive/s06e05", null, false)]
+ [InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
+ [InlineData(@"/Drive/extras", 0, true)]
+ [InlineData(@"/Drive/specials", 0, true)]
+ public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
{
var result = SeasonPathParser.Parse(path, true, true);
+ Assert.Equal(result.SeasonNumber != null, result.Success);
Assert.Equal(result.SeasonNumber, seasonNumber);
+ Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
index 40b41b9f3..89579c037 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
@@ -1,4 +1,5 @@
-using Emby.Naming.Common;
+using System.IO;
+using Emby.Naming.Common;
using Emby.Naming.TV;
using Xunit;
@@ -15,7 +16,6 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("/server/The Walking Dead 4x01.mp4", "The Walking Dead", 4, 1)]
[InlineData("/server/the_simpsons-s02e01_18536.mp4", "the_simpsons", 2, 1)]
[InlineData("/server/Temp/S01E02 foo.mp4", "", 1, 2)]
- [InlineData("Series/4-12 - The Woman.mp4", "", 4, 12)]
[InlineData("Series/4x12 - The Woman.mp4", "", 4, 12)]
[InlineData("Series/LA X, Pt. 1_s06e32.mp4", "LA X, Pt. 1", 6, 32)]
[InlineData("[Baz-Bar]Foo - [1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
@@ -24,16 +24,37 @@ namespace Jellyfin.Naming.Tests.TV
// TODO: [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
// TODO: [InlineData("E:\\Anime\\Yahari Ore no Seishun Love Comedy wa Machigatteiru\\Yahari Ore no Seishun Love Comedy wa Machigatteiru. Zoku\\Oregairu Zoku 11 - Hayama Hayato Always Renconds to Everyone's Expectations..mkv", "Yahari Ore no Seishun Love Comedy wa Machigatteiru", null, 11)]
// TODO: [InlineData(@"/Library/Series/The Grand Tour (2016)/Season 1/S01E01 The Holy Trinity.mkv", "The Grand Tour", 1, 1)]
- public void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber)
+ public void TestSimple(string path, string seriesName, int? seasonNumber, int? episodeNumber)
+ {
+ Test(path, seriesName, seasonNumber, episodeNumber, null);
+ }
+
+ [Theory]
+ [InlineData("Series/4-12 - The Woman.mp4", "", 4, 12, 12)]
+ public void TestWithPossibleEpisodeEnd(string path, string seriesName, int? seasonNumber, int? episodeNumber, int? episodeEndNumber)
+ {
+ Test(path, seriesName, seasonNumber, episodeNumber, episodeEndNumber);
+ }
+
+ private void Test(string path, string seriesName, int? seasonNumber, int? episodeNumber, int? episodeEndNumber)
{
var options = new NamingOptions();
var result = new EpisodeResolver(options)
.Resolve(path, false);
+ Assert.NotNull(result);
Assert.Equal(seasonNumber, result?.SeasonNumber);
Assert.Equal(episodeNumber, result?.EpisodeNumber);
Assert.Equal(seriesName, result?.SeriesName, true);
+ Assert.Equal(path, result?.Path);
+ Assert.Equal(Path.GetExtension(path).Substring(1), result?.Container);
+ Assert.Null(result?.Format3D);
+ Assert.False(result?.Is3D);
+ Assert.False(result?.IsStub);
+ Assert.Null(result?.StubType);
+ Assert.Equal(episodeEndNumber, result?.EndingEpisodeNumber);
+ Assert.False(result?.IsByDate);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
index ed971eed7..15cb5c72f 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Xunit;
@@ -51,6 +51,8 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)]
[InlineData("My Movie 20131209", "My Movie 20131209", null)]
[InlineData("My Movie 2013-12-09 2013", "My Movie 2013-12-09", 2013)]
+ [InlineData(null, null, null)]
+ [InlineData("", "", null)]
public void CleanDateTimeTest(string input, string expectedName, int? expectedYear)
{
input = Path.GetFileName(input);
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index 8dfb8f859..d34f65409 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -1,7 +1,9 @@
-using Emby.Naming.Common;
+using System;
+using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
using Xunit;
+using MediaType = Emby.Naming.Common.MediaType;
namespace Jellyfin.Naming.Tests.Video
{
@@ -93,6 +95,23 @@ namespace Jellyfin.Naming.Tests.Video
}
}
+ [Fact]
+ public void TestExtraInfo_InvalidRuleType()
+ {
+ var rule = new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, @"([eE]x(tra)?\.\w+)", MediaType.Video);
+ var options = new NamingOptions { VideoExtraRules = new[] { rule } };
+ var res = GetExtraTypeParser(options).GetExtraInfo("extra.mp4");
+
+ Assert.Equal(rule, res.Rule);
+ }
+
+ [Fact]
+ public void TestFlagsParser()
+ {
+ var flags = new FlagParser(_videoOptions).GetFlags(string.Empty);
+ Assert.Empty(flags);
+ }
+
private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
{
return new ExtraResolver(videoOptions);
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 4198d69ff..9df6904ef 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.IO;
@@ -11,8 +12,8 @@ namespace Jellyfin.Naming.Tests.Video
private readonly NamingOptions _namingOptions = new NamingOptions();
// FIXME
- // [Fact]
- private void TestMultiEdition1()
+ [Fact]
+ public void TestMultiEdition1()
{
var files = new[]
{
@@ -35,8 +36,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
- // [Fact]
- private void TestMultiEdition2()
+ [Fact]
+ public void TestMultiEdition2()
{
var files = new[]
{
@@ -81,8 +82,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
- // [Fact]
- private void TestLetterFolders()
+ [Fact]
+ public void TestLetterFolders()
{
var files = new[]
{
@@ -109,8 +110,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
- // [Fact]
- private void TestMultiVersionLimit()
+ [Fact]
+ public void TestMultiVersionLimit()
{
var files = new[]
{
@@ -138,8 +139,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
- // [Fact]
- private void TestMultiVersionLimit2()
+ [Fact]
+ public void TestMultiVersionLimit2()
{
var files = new[]
{
@@ -168,8 +169,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
- // [Fact]
- private void TestMultiVersion3()
+ [Fact]
+ public void TestMultiVersion3()
{
var files = new[]
{
@@ -194,8 +195,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
- // [Fact]
- private void TestMultiVersion4()
+ [Fact]
+ public void TestMultiVersion4()
{
// Test for false positive
@@ -221,9 +222,8 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Empty(result[0].AlternateVersions);
}
- // FIXME
- // [Fact]
- private void TestMultiVersion5()
+ [Fact]
+ public void TestMultiVersion5()
{
var files = new[]
{
@@ -254,8 +254,8 @@ namespace Jellyfin.Naming.Tests.Video
}
// FIXME
- // [Fact]
- private void TestMultiVersion6()
+ [Fact]
+ public void TestMultiVersion6()
{
var files = new[]
{
@@ -285,9 +285,8 @@ namespace Jellyfin.Naming.Tests.Video
Assert.True(result[0].AlternateVersions[5].Is3D);
}
- // FIXME
- // [Fact]
- private void TestMultiVersion7()
+ [Fact]
+ public void TestMultiVersion7()
{
var files = new[]
{
@@ -306,12 +305,9 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(2, result.Count);
}
- // FIXME
- // [Fact]
- private void TestMultiVersion8()
+ [Fact]
+ public void TestMultiVersion8()
{
- // This is not actually supported yet
-
var files = new[]
{
@"/movies/Iron Man/Iron Man.mkv",
@@ -339,9 +335,8 @@ namespace Jellyfin.Naming.Tests.Video
Assert.True(result[0].AlternateVersions[4].Is3D);
}
- // FIXME
- // [Fact]
- private void TestMultiVersion9()
+ [Fact]
+ public void TestMultiVersion9()
{
// Test for false positive
@@ -367,9 +362,8 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Empty(result[0].AlternateVersions);
}
- // FIXME
- // [Fact]
- private void TestMultiVersion10()
+ [Fact]
+ public void TestMultiVersion10()
{
var files = new[]
{
@@ -390,12 +384,9 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Single(result[0].AlternateVersions);
}
- // FIXME
- // [Fact]
- private void TestMultiVersion11()
+ [Fact]
+ public void TestMultiVersion11()
{
- // Currently not supported but we should probably handle this.
-
var files = new[]
{
@"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
@@ -415,6 +406,16 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Single(result[0].AlternateVersions);
}
+ [Fact]
+ public void TestEmptyList()
+ {
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(new List<FileSystemMetadata>()).ToList();
+
+ Assert.Empty(result);
+ }
+
private VideoListResolver GetResolver()
{
return new VideoListResolver(_namingOptions);
diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
index 30ba94136..6e759c6d6 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
@@ -1,4 +1,4 @@
-using Emby.Naming.Common;
+using Emby.Naming.Common;
using Emby.Naming.Video;
using Xunit;
@@ -23,6 +23,7 @@ namespace Jellyfin.Naming.Tests.Video
Test("video.hdtv.disc", true, "tv");
Test("video.pdtv.disc", true, "tv");
Test("video.dsr.disc", true, "tv");
+ Test(string.Empty, false, "tv");
}
[Fact]
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 12c4a50fe..215c7e540 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.IO;
@@ -370,6 +370,26 @@ namespace Jellyfin.Naming.Tests.Video
}
[Fact]
+ public void TestFourRooms()
+ {
+ var files = new[]
+ {
+ @"Four Rooms - A.avi",
+ @"Four Rooms - A.mp4"
+ };
+
+ var resolver = GetResolver();
+
+ var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList()).ToList();
+
+ Assert.Equal(2, result.Count);
+ }
+
+ [Fact]
public void TestMovieTrailer()
{
var files = new[]
@@ -431,6 +451,13 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Single(result);
}
+ [Fact]
+ public void TestDirectoryStack()
+ {
+ var stack = new FileStack();
+ Assert.False(stack.ContainsFile("XX", true));
+ }
+
private VideoListResolver GetResolver()
{
return new VideoListResolver(_namingOptions);
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index 99828b2eb..3bdafa84d 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
+using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
@@ -14,165 +15,135 @@ namespace Jellyfin.Naming.Tests.Video
{
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
- Container = "mkv",
- Name = "7 Psychos"
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
+ container: "mkv",
+ name: "7 Psychos")
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
- Container = "mkv",
- Name = "3 days to kill",
- Year = 2005
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
+ container: "mkv",
+ name: "3 days to kill",
+ year: 2005)
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/American Psycho/American.Psycho.mkv",
- Container = "mkv",
- Name = "American.Psycho",
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/American Psycho/American.Psycho.mkv",
+ container: "mkv",
+ name: "American.Psycho")
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
- Container = "mkv",
- Name = "brave",
- Year = 2006,
- Is3D = true,
- Format3D = "sbs",
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
+ container: "mkv",
+ name: "brave",
+ year: 2006,
+ is3D: true,
+ format3D: "sbs")
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
- Container = "mkv",
- Name = "300",
- Year = 2006
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
+ container: "mkv",
+ name: "300",
+ year: 2006)
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
- Container = "mkv",
- Name = "300",
- Year = 2006,
- Is3D = true,
- Format3D = "sbs",
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
+ container: "mkv",
+ name: "300",
+ year: 2006,
+ is3D: true,
+ format3D: "sbs")
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
- Container = "disc",
- Name = "brave",
- Year = 2006,
- IsStub = true,
- StubType = "bluray",
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
+ container: "disc",
+ name: "brave",
+ year: 2006,
+ isStub: true,
+ stubType: "bluray")
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
- Container = "disc",
- Name = "300",
- Year = 2006,
- IsStub = true,
- StubType = "bluray",
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
+ container: "disc",
+ name: "300",
+ year: 2006,
+ isStub: true,
+ stubType: "bluray")
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
- Container = "disc",
- Name = "Brave",
- Year = 2006,
- IsStub = true,
- StubType = "bluray",
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
+ container: "disc",
+ name: "Brave",
+ year: 2006,
+ isStub: true,
+ stubType: "bluray")
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
- Container = "disc",
- Name = "300",
- Year = 2006,
- IsStub = true,
- StubType = "bluray",
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
+ container: "disc",
+ name: "300",
+ year: 2006,
+ isStub: true,
+ stubType: "bluray")
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
- Container = "mkv",
- Name = "300",
- Year = 2006,
- ExtraType = ExtraType.Trailer,
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
+ container: "mkv",
+ name: "300",
+ year: 2006,
+ extraType: ExtraType.Trailer)
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
- Container = "mkv",
- Name = "Brave",
- Year = 2006,
- ExtraType = ExtraType.Trailer,
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
+ container: "mkv",
+ name: "Brave",
+ year: 2006,
+ extraType: ExtraType.Trailer)
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/300 (2007)/300 (2006).mkv",
- Container = "mkv",
- Name = "300",
- Year = 2006
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/300 (2007)/300 (2006).mkv",
+ container: "mkv",
+ name: "300",
+ year: 2006)
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
- Container = "mkv",
- Name = "Bad Boys",
- Year = 1995,
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
+ container: "mkv",
+ name: "Bad Boys",
+ year: 1995)
};
yield return new object[]
{
- new VideoFileInfo()
- {
- Path = @"/server/Movies/Brave (2007)/Brave (2006).mkv",
- Container = "mkv",
- Name = "Brave",
- Year = 2006,
- }
+ new VideoFileInfo(
+ path: @"/server/Movies/Brave (2007)/Brave (2006).mkv",
+ container: "mkv",
+ name: "Brave",
+ year: 2006)
};
}
@@ -194,6 +165,34 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(result?.StubType, expectedResult.StubType);
Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory);
Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
+ Assert.Equal(result?.ToString(), expectedResult.ToString());
+ }
+
+ [Fact]
+ public void ResolveFile_EmptyPath()
+ {
+ var result = new VideoResolver(_namingOptions).ResolveFile(string.Empty);
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ResolveDirectoryTest()
+ {
+ var paths = new[]
+ {
+ @"/Server/Iron Man",
+ @"Batman",
+ string.Empty
+ };
+
+ var resolver = new VideoResolver(_namingOptions);
+ var results = paths.Select(path => resolver.ResolveDirectory(path)).ToList();
+
+ Assert.Equal(3, results.Count);
+ Assert.NotNull(results[0]);
+ Assert.NotNull(results[1]);
+ Assert.Null(results[2]);
}
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index db1f2956e..547f80ed9 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
@@ -17,7 +17,7 @@
<PackageReference Include="AutoFixture" Version="4.14.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
- <PackageReference Include="Moq" Version="4.14.7" />
+ <PackageReference Include="Moq" Version="4.15.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />