aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-abi.yml2
-rw-r--r--.ci/azure-pipelines-main.yml2
-rw-r--r--.ci/azure-pipelines-package.yml4
-rw-r--r--.ci/azure-pipelines-test.yml4
-rw-r--r--.config/dotnet-tools.json12
-rw-r--r--.github/workflows/ci-codeql-analysis.yml (renamed from .github/workflows/codeql-analysis.yml)12
-rw-r--r--.github/workflows/ci-openapi.yml (renamed from .github/workflows/openapi.yml)28
-rw-r--r--.github/workflows/ci-tests.yml44
-rw-r--r--.github/workflows/commands.yml14
-rw-r--r--.github/workflows/issue-stale.yml (renamed from .github/workflows/repo-stale.yaml)29
-rw-r--r--.github/workflows/project-automation.yml (renamed from .github/workflows/automation.yml)15
-rw-r--r--.github/workflows/pull-request-conflict.yml23
-rw-r--r--.github/workflows/pull-request-stale.yaml30
-rw-r--r--.github/workflows/release-bump-version.yaml (renamed from .github/workflows/repo-bump-version.yaml)4
-rw-r--r--.vscode/launch.json4
-rw-r--r--CONTRIBUTORS.md6
-rw-r--r--Directory.Packages.props85
-rw-r--r--Dockerfile2
-rw-r--r--Dockerfile.arm2
-rw-r--r--Dockerfile.arm642
-rw-r--r--Emby.Dlna/Common/Argument.cs23
-rw-r--r--Emby.Dlna/Common/DeviceIcon.cs41
-rw-r--r--Emby.Dlna/Common/DeviceService.cs36
-rw-r--r--Emby.Dlna/Common/ServiceAction.cs31
-rw-r--r--Emby.Dlna/Common/StateVariable.cs34
-rw-r--r--Emby.Dlna/Configuration/DlnaOptions.cs92
-rw-r--r--Emby.Dlna/ConfigurationExtension.cs15
-rw-r--r--Emby.Dlna/ConnectionManager/ConnectionManagerService.cs53
-rw-r--r--Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs119
-rw-r--r--Emby.Dlna/ConnectionManager/ControlHandler.cs55
-rw-r--r--Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs234
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectoryService.cs173
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs159
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs1262
-rw-r--r--Emby.Dlna/ContentDirectory/ServerItem.cs39
-rw-r--r--Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs415
-rw-r--r--Emby.Dlna/ContentDirectory/StubType.cs30
-rw-r--r--Emby.Dlna/ControlRequest.cs25
-rw-r--r--Emby.Dlna/ControlResponse.cs28
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs1266
-rw-r--r--Emby.Dlna/Didl/Filter.cs28
-rw-r--r--Emby.Dlna/Didl/StringWriterWithEncoding.cs58
-rw-r--r--Emby.Dlna/DlnaConfigurationFactory.cs23
-rw-r--r--Emby.Dlna/DlnaManager.cs491
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj86
-rw-r--r--Emby.Dlna/EventSubscriptionResponse.cs22
-rw-r--r--Emby.Dlna/Eventing/DlnaEventManager.cs183
-rw-r--r--Emby.Dlna/Eventing/EventSubscription.cs35
-rw-r--r--Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs69
-rw-r--r--Emby.Dlna/IConnectionManager.cs8
-rw-r--r--Emby.Dlna/IContentDirectory.cs8
-rw-r--r--Emby.Dlna/IDlnaEventManager.cs34
-rw-r--r--Emby.Dlna/IMediaReceiverRegistrar.cs8
-rw-r--r--Emby.Dlna/IUpnpService.cs22
-rw-r--r--Emby.Dlna/Images/logo120.jpgbin5337 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/logo120.pngbin6201 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/logo240.jpgbin11483 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/logo240.pngbin13339 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/logo48.jpgbin1839 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/logo48.pngbin2263 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/people48.jpgbin740 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/people48.pngbin278 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/people480.jpgbin5181 -> 0 bytes
-rw-r--r--Emby.Dlna/Images/people480.pngbin2105 -> 0 bytes
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs363
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs58
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs46
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs90
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs187
-rw-r--r--Emby.Dlna/PlayTo/Device.cs1269
-rw-r--r--Emby.Dlna/PlayTo/DeviceInfo.cs66
-rw-r--r--Emby.Dlna/PlayTo/DlnaHttpClient.cs137
-rw-r--r--Emby.Dlna/PlayTo/MediaChangedEventArgs.cs19
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs980
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs258
-rw-r--r--Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs16
-rw-r--r--Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs16
-rw-r--r--Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs16
-rw-r--r--Emby.Dlna/PlayTo/PlaylistItem.cs19
-rw-r--r--Emby.Dlna/PlayTo/PlaylistItemFactory.cs70
-rw-r--r--Emby.Dlna/PlayTo/TransportCommands.cs181
-rw-r--r--Emby.Dlna/PlayTo/TransportState.cs16
-rw-r--r--Emby.Dlna/PlayTo/UpnpContainer.cs25
-rw-r--r--Emby.Dlna/PlayTo/uBaseObject.cs62
-rw-r--r--Emby.Dlna/PlayTo/uPnpNamespaces.cs67
-rw-r--r--Emby.Dlna/Profiles/DefaultProfile.cs179
-rw-r--r--Emby.Dlna/Profiles/Xml/Default.xml61
-rw-r--r--Emby.Dlna/Profiles/Xml/Denon AVR.xml68
-rw-r--r--Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml67
-rw-r--r--Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml96
-rw-r--r--Emby.Dlna/Profiles/Xml/LG Smart TV.xml92
-rw-r--r--Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml54
-rw-r--r--Emby.Dlna/Profiles/Xml/Marantz.xml62
-rw-r--r--Emby.Dlna/Profiles/Xml/MediaMonkey.xml62
-rw-r--r--Emby.Dlna/Profiles/Xml/Panasonic Viera.xml87
-rw-r--r--Emby.Dlna/Profiles/Xml/Popcorn Hour.xml92
-rw-r--r--Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml128
-rw-r--r--Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml60
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml87
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml87
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml85
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml85
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml115
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml133
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml139
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml115
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml114
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml114
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml105
-rw-r--r--Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml108
-rw-r--r--Emby.Dlna/Profiles/Xml/WDTV Live.xml94
-rw-r--r--Emby.Dlna/Profiles/Xml/Xbox One.xml126
-rw-r--r--Emby.Dlna/Profiles/Xml/foobar2000.xml67
-rw-r--r--Emby.Dlna/Properties/AssemblyInfo.cs28
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs358
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs242
-rw-r--r--Emby.Dlna/Service/BaseService.cs37
-rw-r--r--Emby.Dlna/Service/ControlErrorHandler.cs52
-rw-r--r--Emby.Dlna/Service/ServiceXmlBuilder.cs109
-rw-r--r--Emby.Dlna/Ssdp/DeviceDiscovery.cs151
-rw-r--r--Emby.Dlna/Ssdp/SsdpExtensions.cs27
-rw-r--r--Emby.Naming/Common/NamingOptions.cs10
-rw-r--r--Emby.Naming/Emby.Naming.csproj12
-rw-r--r--Emby.Photos/Emby.Photos.csproj10
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs6
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs12
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs2
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs228
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs11
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj11
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs208
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs668
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs154
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs1
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs1
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs2
-rw-r--r--Emby.Server.Implementations/Images/BaseFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs60
-rw-r--r--Emby.Server.Implementations/Images/DynamicImageProvider.cs12
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs79
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs15
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs14
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs62
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs9
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs47
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs49
-rw-r--r--Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ab.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/eu.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json16
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json42
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json78
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/si.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json22
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json4
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs119
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs15
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs2
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs11
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs5
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs97
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/ArtistComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/CriticRatingComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/DateCreatedComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/IndexNumberComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/IsFolderComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/IsPlayedComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/NameComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/PremiereDateComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/ProductionYearComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/RandomComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs3
-rw-r--r--Emby.Server.Implementations/SystemManager.cs4
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs138
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs15
-rw-r--r--Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs25
-rw-r--r--Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs29
-rw-r--r--Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs29
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs5
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs8
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs7
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs1
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs1
-rw-r--r--Jellyfin.Api/Controllers/DlnaController.cs132
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs329
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs44
-rw-r--r--Jellyfin.Api/Controllers/EnvironmentController.cs3
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs2
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs6
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs19
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs10
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs80
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs29
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs1
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs5
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs1
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs4
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs1
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs1
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ScheduledTasksController.cs1
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs15
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs7
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs1
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs1
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs4
-rw-r--r--Jellyfin.Api/Controllers/TrickplayController.cs101
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs1
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs26
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs8
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs22
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs22
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs66
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs2
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs6
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs242
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs4
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj10
-rw-r--r--Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs2
-rw-r--r--Jellyfin.Api/Middleware/ExceptionMiddleware.cs20
-rw-r--r--Jellyfin.Api/Middleware/LanFilteringMiddleware.cs1
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs2
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs3
-rw-r--r--Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs3
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamState.cs20
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs5
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs7
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs5
-rw-r--r--Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs7
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs17
-rw-r--r--Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs11
-rw-r--r--Jellyfin.Data/Entities/TrickplayInfo.cs75
-rw-r--r--Jellyfin.Data/Entities/User.cs7
-rw-r--r--Jellyfin.Data/Enums/CollectionType.cs165
-rw-r--r--Jellyfin.Data/Enums/ItemSortBy.cs167
-rw-r--r--Jellyfin.Data/Enums/MediaType.cs32
-rw-r--r--Jellyfin.Data/Enums/PermissionKind.cs7
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj12
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfiguration.cs176
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs20
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs23
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs24
-rw-r--r--Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs120
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs1127
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs10
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs1
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs7
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs7
-rw-r--r--Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs3
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj8
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs5
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs681
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs40
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs654
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs29
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs39
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs18
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthenticationManager.cs13
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs2
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs474
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs9
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs29
-rw-r--r--Jellyfin.Server/CoreAppHost.cs3
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs2
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs20
-rw-r--r--Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs42
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj8
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs55
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs7
-rw-r--r--Jellyfin.Server/Program.cs2
-rw-r--r--Jellyfin.Server/Startup.cs5
-rw-r--r--Jellyfin.sln22
-rw-r--r--MediaBrowser.Common/Api/Policies.cs (renamed from Jellyfin.Api/Constants/Policies.cs)7
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj9
-rw-r--r--MediaBrowser.Common/Net/INetworkManager.cs2
-rw-r--r--MediaBrowser.Common/Net/NetworkConfiguration.cs175
-rw-r--r--MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs19
-rw-r--r--MediaBrowser.Common/Net/NetworkConfigurationFactory.cs22
-rw-r--r--MediaBrowser.Common/Net/NetworkConfigurationStore.cs23
-rw-r--r--MediaBrowser.Common/Net/NetworkConstants.cs (renamed from Jellyfin.Networking/Constants/Network.cs)6
-rw-r--r--MediaBrowser.Common/Net/NetworkUtils.cs (renamed from Jellyfin.Networking/Extensions/NetworkExtensions.cs)67
-rw-r--r--MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs19
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs1
-rw-r--r--MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs1
-rw-r--r--MediaBrowser.Controller/Dlna/IDlnaManager.cs80
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs10
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs5
-rw-r--r--MediaBrowser.Controller/Entities/BasePluginFolder.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Book.cs5
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/ICollectionFolder.cs3
-rw-r--r--MediaBrowser.Controller/Entities/IHasDisplayOrder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs8
-rw-r--r--MediaBrowser.Controller/Entities/Photo.cs3
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs37
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs86
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs3
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs24
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs9
-rw-r--r--MediaBrowser.Controller/Library/IUserViewManager.cs3
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs7
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs2
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj9
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs103
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs32
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs2
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs14
-rw-r--r--MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs19
-rw-r--r--MediaBrowser.Controller/Providers/EpisodeInfo.cs1
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs1
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs3
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs11
-rw-r--r--MediaBrowser.Controller/Sorting/IBaseItemComparer.cs6
-rw-r--r--MediaBrowser.Controller/Trickplay/ITrickplayManager.cs76
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj8
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs8
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs1
-rw-r--r--MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs5
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs26
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs238
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj8
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs19
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs10
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs6
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs4
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs510
-rw-r--r--MediaBrowser.Model/Configuration/TrickplayOptions.cs60
-rw-r--r--MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs17
-rw-r--r--MediaBrowser.Model/Configuration/UserConfiguration.cs5
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs259
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs8
-rw-r--r--MediaBrowser.Model/Dlna/DlnaFlags.cs50
-rw-r--r--MediaBrowser.Model/Dlna/DlnaMaps.cs46
-rw-r--r--MediaBrowser.Model/Dlna/IDeviceDiscovery.cs14
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfile.cs114
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs532
-rw-r--r--MediaBrowser.Model/Dlna/SearchCriteria.cs55
-rw-r--r--MediaBrowser.Model/Dlna/SearchType.cs14
-rw-r--r--MediaBrowser.Model/Dlna/SortCriteria.cs24
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs13
-rw-r--r--MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs22
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs11
-rw-r--r--MediaBrowser.Model/Dto/MetadataEditorInfo.cs3
-rw-r--r--MediaBrowser.Model/Dto/UpdateUserItemDataDto.cs76
-rw-r--r--MediaBrowser.Model/Entities/CollectionType.cs27
-rw-r--r--MediaBrowser.Model/Entities/MediaType.cs28
-rw-r--r--MediaBrowser.Model/Entities/UserDataSaveReason.cs7
-rw-r--r--MediaBrowser.Model/Library/UserViewQuery.cs5
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs4
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj10
-rw-r--r--MediaBrowser.Model/Net/IPData.cs2
-rw-r--r--MediaBrowser.Model/Net/ISocketFactory.cs18
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs3
-rw-r--r--MediaBrowser.Model/Providers/ExternalIdMediaType.cs7
-rw-r--r--MediaBrowser.Model/Querying/ItemFields.cs5
-rw-r--r--MediaBrowser.Model/Querying/ItemSortBy.cs163
-rw-r--r--MediaBrowser.Model/Search/SearchHint.cs4
-rw-r--r--MediaBrowser.Model/Search/SearchQuery.cs4
-rw-r--r--MediaBrowser.Model/Session/ClientCapabilities.cs5
-rw-r--r--MediaBrowser.Model/System/CastReceiverApplication.cs17
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs6
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs11
-rw-r--r--MediaBrowser.Providers/Lyric/LrcLyricParser.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs10
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj10
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs5
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs2
-rw-r--r--MediaBrowser.Providers/Movies/ImdbExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs60
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs1
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/TV/Zap2ItExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs117
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayProvider.cs121
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj6
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs4
-rw-r--r--README.md4
-rw-r--r--RSSDP/DeviceAvailableEventArgs.cs50
-rw-r--r--RSSDP/DeviceEventArgs.cs35
-rw-r--r--RSSDP/DeviceUnavailableEventArgs.cs47
-rw-r--r--RSSDP/DiscoveredSsdpDevice.cs74
-rw-r--r--RSSDP/DisposableManagedObjectBase.cs76
-rw-r--r--RSSDP/HttpParserBase.cs228
-rw-r--r--RSSDP/HttpRequestParser.cs91
-rw-r--r--RSSDP/HttpResponseParser.cs93
-rw-r--r--RSSDP/IEnumerableExtensions.cs34
-rw-r--r--RSSDP/ISsdpCommunicationsServer.cs52
-rw-r--r--RSSDP/ISsdpDeviceLocator.cs123
-rw-r--r--RSSDP/ISsdpDevicePublisher.cs35
-rw-r--r--RSSDP/LICENSE4
-rw-r--r--RSSDP/Properties/AssemblyInfo.cs24
-rw-r--r--RSSDP/RSSDP.csproj21
-rw-r--r--RSSDP/RequestReceivedEventArgs.cs44
-rw-r--r--RSSDP/ResponseReceivedEventArgs.cs43
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs529
-rw-r--r--RSSDP/SsdpConstants.cs63
-rw-r--r--RSSDP/SsdpDevice.cs361
-rw-r--r--RSSDP/SsdpDeviceLocator.cs632
-rw-r--r--RSSDP/SsdpDevicePublisher.cs629
-rw-r--r--RSSDP/SsdpEmbeddedDevice.cs40
-rw-r--r--RSSDP/SsdpRootDevice.cs71
-rw-r--r--debian/conf/jellyfin3
-rw-r--r--debian/control2
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm642
-rw-r--r--deployment/Dockerfile.debian.armhf2
-rw-r--r--deployment/Dockerfile.docker.amd642
-rw-r--r--deployment/Dockerfile.docker.arm642
-rw-r--r--deployment/Dockerfile.docker.armhf2
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.linux.amd64-musl2
-rw-r--r--deployment/Dockerfile.linux.arm642
-rw-r--r--deployment/Dockerfile.linux.armhf2
-rw-r--r--deployment/Dockerfile.linux.musl-linux-arm642
-rw-r--r--deployment/Dockerfile.macos.amd642
-rw-r--r--deployment/Dockerfile.macos.arm642
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rwxr-xr-xdeployment/build.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--deployment/unraid/docker-templates/jellyfin.xml2
-rw-r--r--fedora/jellyfin.env6
-rw-r--r--fedora/jellyfin.spec4
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj2
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Program.cs9
-rwxr-xr-xfuzz/Emby.Server.Implementations.Fuzz/fuzz.sh2
-rw-r--r--fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj (renamed from fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj)6
-rw-r--r--fuzz/Jellyfin.Api.Fuzz/Program.cs (renamed from fuzz/Jellyfin.Server.Fuzz/Program.cs)4
-rw-r--r--fuzz/Jellyfin.Api.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt (renamed from fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt)0
-rwxr-xr-xfuzz/Jellyfin.Api.Fuzz/fuzz.sh11
-rwxr-xr-xfuzz/Jellyfin.Server.Fuzz/fuzz.sh11
-rw-r--r--fuzz/README.md20
-rw-r--r--global.json2
-rw-r--r--jellyfin.ruleset3
-rw-r--r--src/Directory.Build.props (renamed from Jellyfin.Networking/Jellyfin.Networking.csproj)22
-rw-r--r--src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj23
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs134
-rw-r--r--src/Jellyfin.Drawing/Jellyfin.Drawing.csproj17
-rw-r--r--src/Jellyfin.Drawing/NullImageEncoder.cs6
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj18
-rw-r--r--src/Jellyfin.Extensions/Json/JsonDefaults.cs4
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj17
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs7
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj17
-rw-r--r--src/Jellyfin.Networking/ExternalPortForwarding.cs195
-rw-r--r--src/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs119
-rw-r--r--src/Jellyfin.Networking/Jellyfin.Networking.csproj20
-rw-r--r--src/Jellyfin.Networking/Manager/NetworkManager.cs1125
-rw-r--r--src/Jellyfin.Networking/Udp/SocketFactory.cs38
-rw-r--r--src/Jellyfin.Networking/Udp/UdpServer.cs136
-rw-r--r--src/Jellyfin.Networking/UdpServerEntryPoint.cs143
-rw-r--r--tests/Directory.Build.props2
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs120
-rw-r--r--tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs36
-rw-r--r--tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs (renamed from tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs)3
-rw-r--r--tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs131
-rw-r--r--tests/Jellyfin.Dlna.Tests/GetUuidTests.cs17
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj18
-rw-r--r--tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs47
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs65
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs43
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs50
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs8
-rw-r--r--tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs6
-rw-r--r--tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs56
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs10
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs20
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs8
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs114
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs34
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs60
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs172
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StackTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StubTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs56
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs36
-rw-r--r--tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs2
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj2
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs10
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs2
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs23
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs18
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs8
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs3
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs5
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs24
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs18
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs111
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs11
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs2
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs154
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs21
-rw-r--r--tests/Jellyfin.Server.Tests/ParseNetworkTests.cs5
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs2
624 files changed, 8721 insertions, 23612 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 4b82eedb4..547a514f8 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: 7.0.x
+ default: 8.0.x
jobs:
- job: CompatibilityCheck
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
index 020d7fff4..0702aeb6b 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: 7.0.x
+ DotNetSdkVersion: 8.0.x
jobs:
- job: Build
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index c91a084e5..39f98e063 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -208,10 +208,10 @@ jobs:
steps:
- task: UseDotNet@2
- displayName: 'Use .NET 7.0 sdk'
+ displayName: 'Use .NET 8.0 sdk'
inputs:
packageType: 'sdk'
- version: '7.0.x'
+ version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 81362aab2..3549c691c 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: 7.0.x
+ default: 8.0.x
jobs:
- job: Test
@@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
- targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json"
+ targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 000000000..c03564f97
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "8.0.0",
+ "commands": [
+ "dotnet-ef"
+ ]
+ }
+ }
+}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 8385e84bf..89f9c59d7 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup .NET
- uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
+ uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
- dotnet-version: '7.0.x'
+ dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
+ uses: github/codeql-action/init@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
+ uses: github/codeql-action/autobuild@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
+ uses: github/codeql-action/analyze@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11
diff --git a/.github/workflows/openapi.yml b/.github/workflows/ci-openapi.yml
index 693f98d16..5ff9820cb 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -14,23 +14,23 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
+ uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
- dotnet-version: '7.0.x'
+ dotnet-version: '8.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
+ uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
- path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
openapi-base:
name: OpenAPI - BASE
@@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -53,18 +53,18 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
+ uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
- dotnet-version: '7.0.x'
+ dotnet-version: '8.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
+ uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
- path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net7.0/openapi.json
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
openapi-diff:
permissions:
@@ -78,12 +78,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
+ uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
+ uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0
with:
name: openapi-base
path: openapi-base
@@ -112,7 +112,7 @@ jobs:
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -127,7 +127,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
new file mode 100644
index 000000000..0dacbc5c6
--- /dev/null
+++ b/.github/workflows/ci-tests.yml
@@ -0,0 +1,44 @@
+name: Tests
+on:
+ push:
+ branches:
+ - master
+ # Run tests against the forked branch, but
+ # do not allow access to secrets
+ # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories
+ pull_request:
+
+env:
+ SDK_VERSION: "8.0.x"
+
+jobs:
+ run-tests:
+ strategy:
+ matrix:
+ os: ["ubuntu-latest", "macos-latest", "windows-latest"]
+
+ runs-on: "${{ matrix.os }}"
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
+ with:
+ dotnet-version: ${{ env.SDK_VERSION }}
+
+ - name: Run DotNet CLI Tests
+ run: >
+ dotnet test Jellyfin.sln
+ --configuration Release
+ --collect:"XPlat Code Coverage"
+ --settings tests/coverletArgs.runsettings
+ --verbosity minimal
+
+ - name: Merge code coverage results
+ uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5.2.0
+ with:
+ reports: "**/coverage.cobertura.xml"
+ targetdir: "merged/"
+ reporttypes: "Cobertura"
+
+ # TODO - which action / tool to use to publish code coverage results?
+ # - name: Publish code coverage results
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index ba7883a73..75b6a73e5 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,14 +51,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/issue-stale.yml
index 2b1164116..5a1ca9f7a 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/issue-stale.yml
@@ -1,8 +1,8 @@
-name: Stale Check
+name: Stale Issue Labeler
on:
schedule:
- - cron: '30 */12 * * *'
+ - cron: '30 1 * * *'
workflow_dispatch:
permissions:
@@ -16,37 +16,20 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
+ - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
+ ascending: true
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
- operations-per-run: 75
+ operations-per-run: 500
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
- This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
+ This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
close-issue-message: |-
This issue was closed due to inactivity.
-
- prs-conflicts:
- name: Check PRs with merge conflicts
- runs-on: ubuntu-latest
- if: ${{ contains(github.repository, 'jellyfin/') }}
- steps:
- - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
- with:
- repo-token: ${{ secrets.JF_BOT_TOKEN }}
- operations-per-run: 75
- # The merge conflict action will remove the label when updated
- remove-stale-when-updated: false
- days-before-stale: -1
- days-before-close: 90
- days-before-issue-close: -1
- stale-pr-label: merge conflict
- close-pr-message: |-
- This PR has been closed due to having unresolved merge conflicts.
diff --git a/.github/workflows/automation.yml b/.github/workflows/project-automation.yml
index 47abce02a..3637eb16a 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/project-automation.yml
@@ -1,4 +1,4 @@
-name: Automation
+name: Project Automation
on:
push:
@@ -9,19 +9,6 @@ on:
permissions: {}
jobs:
- label:
- name: Labeling
- runs-on: ubuntu-latest
- if: ${{ github.repository == 'jellyfin/jellyfin' }}
- steps:
- - name: Apply label
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
- if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
- with:
- dirtyLabel: 'merge conflict'
- commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
- repoToken: ${{ secrets.JF_BOT_TOKEN }}
-
project:
name: Project board
runs-on: ubuntu-latest
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
new file mode 100644
index 000000000..05517bb03
--- /dev/null
+++ b/.github/workflows/pull-request-conflict.yml
@@ -0,0 +1,23 @@
+name: Merge Conflict Labeler
+
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+ issue_comment:
+
+permissions: {}
+jobs:
+ label:
+ name: Labeling
+ runs-on: ubuntu-latest
+ if: ${{ github.repository == 'jellyfin/jellyfin' }}
+ steps:
+ - name: Apply label
+ uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
+ if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
+ with:
+ dirtyLabel: 'merge conflict'
+ commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
+ repoToken: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml
new file mode 100644
index 000000000..d01b3f4a1
--- /dev/null
+++ b/.github/workflows/pull-request-stale.yaml
@@ -0,0 +1,30 @@
+name: Stale PR Check
+
+on:
+ schedule:
+ - cron: '30 */12 * * *'
+ workflow_dispatch:
+
+permissions:
+ pull-requests: write
+ actions: write
+
+jobs:
+ prs-stale-conflicts:
+ name: Check PRs with merge conflicts
+ runs-on: ubuntu-latest
+ if: ${{ contains(github.repository, 'jellyfin/') }}
+ steps:
+ - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
+ with:
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+ ascending: true
+ operations-per-run: 150
+ # The merge conflict action will remove the label when updated
+ remove-stale-when-updated: false
+ days-before-stale: -1
+ days-before-close: 90
+ days-before-issue-close: -1
+ stale-pr-label: merge conflict
+ close-pr-message: |-
+ This PR has been closed due to having unresolved merge conflicts.
diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/release-bump-version.yaml
index 0ba68dda3..e0383afd2 100644
--- a/.github/workflows/repo-bump-version.yaml
+++ b/.github/workflows/release-bump-version.yaml
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ env.TAG_BRANCH }}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 55e6508a9..be55764fd 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/net7.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.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/net7.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index e3af12a49..d208879d1 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -57,6 +57,7 @@
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
+ - [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
@@ -88,6 +89,7 @@
- [neilsb](https://github.com/neilsb)
- [nevado](https://github.com/nevado)
- [Nickbert7](https://github.com/Nickbert7)
+ - [nicknsy](https://github.com/nicknsy)
- [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka)
- [OancaAndrei](https://github.com/OancaAndrei)
@@ -168,6 +170,10 @@
- [TheTyrius](https://github.com/TheTyrius)
- [tallbl0nde](https://github.com/tallbl0nde)
- [sleepycatcoding](https://github.com/sleepycatcoding)
+ - [scampower3](https://github.com/scampower3)
+ - [Chris-Codes-It] (https://github.com/Chris-Codes-It)
+ - [Pithaya](https://github.com/Pithaya)
+ - [Çağrı Sakaoğlu](https://github.com/ilovepilav)
# Emby Contributors
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 29bf42328..ff76252f8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,13 +2,11 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
-
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
-
<ItemGroup Label="Package Dependencies">
- <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
- <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
- <PackageVersion Include="AutoFixture" Version="4.18.0" />
+ <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
+ <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
+ <PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" />
<PackageVersion Include="BlurHashSharp" Version="1.3.0" />
@@ -17,77 +15,76 @@
<PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
- <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
+ <PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
- <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
+ <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
- <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.0" />
- <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.1" />
+ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
- <PackageVersion Include="prometheus-net" Version="8.0.1" />
- <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
+ <PackageVersion Include="prometheus-net" Version="8.2.0" />
+ <PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
- <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" />
+ <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
- <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
+ <PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
- <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
+ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
<PackageVersion Include="SkiaSharp" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
- <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
+ <PackageVersion Include="Svg.Skia" Version="1.0.0.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
- <PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" />
- <PackageVersion Include="System.Text.Json" Version="7.0.3" />
- <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
+ <PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
+ <PackageVersion Include="System.Text.Json" Version="8.0.0" />
+ <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="TMDbLib" Version="2.0.0" />
+ <PackageVersion Include="TMDbLib" Version="2.1.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageVersion Include="xunit" Version="2.5.3" />
+ <PackageVersion Include="xunit" Version="2.6.1" />
</ItemGroup>
</Project>
diff --git a/Dockerfile b/Dockerfile
index 9be319311..d3f10cd12 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=7.0
+ARG DOTNET_VERSION=8.0
FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
diff --git a/Dockerfile.arm b/Dockerfile.arm
index e8ec6398e..db1acc838 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=7.0
+ARG DOTNET_VERSION=8.0
FROM node:20-alpine as web-builder
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 83137ee89..3eb5f45fc 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=7.0
+ARG DOTNET_VERSION=8.0
FROM node:20-alpine as web-builder
diff --git a/Emby.Dlna/Common/Argument.cs b/Emby.Dlna/Common/Argument.cs
deleted file mode 100644
index e4e9c55e0..000000000
--- a/Emby.Dlna/Common/Argument.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Emby.Dlna.Common
-{
- /// <summary>
- /// DLNA Query parameter type, used when querying DLNA devices via SOAP.
- /// </summary>
- public class Argument
- {
- /// <summary>
- /// Gets or sets name of the DLNA argument.
- /// </summary>
- public string Name { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the direction of the parameter.
- /// </summary>
- public string Direction { get; set; } = string.Empty;
-
- /// <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
deleted file mode 100644
index f9fd1dcec..000000000
--- a/Emby.Dlna/Common/DeviceIcon.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Globalization;
-
-namespace Emby.Dlna.Common
-{
- /// <summary>
- /// Defines the <see cref="DeviceIcon" />.
- /// </summary>
- public class DeviceIcon
- {
- /// <summary>
- /// Gets or sets the Url.
- /// </summary>
- public string Url { get; set; } = string.Empty;
-
- /// <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; }
-
- /// <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);
- }
- }
-}
diff --git a/Emby.Dlna/Common/DeviceService.cs b/Emby.Dlna/Common/DeviceService.cs
deleted file mode 100644
index c1369558e..000000000
--- a/Emby.Dlna/Common/DeviceService.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-namespace Emby.Dlna.Common
-{
- /// <summary>
- /// Defines the <see cref="DeviceService" />.
- /// </summary>
- public class DeviceService
- {
- /// <summary>
- /// Gets or sets the Service Type.
- /// </summary>
- public string ServiceType { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the Service Id.
- /// </summary>
- public string ServiceId { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the Scpd Url.
- /// </summary>
- public string ScpdUrl { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the Control Url.
- /// </summary>
- public string ControlUrl { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the EventSubUrl.
- /// </summary>
- public string EventSubUrl { get; set; } = string.Empty;
-
- /// <inheritdoc />
- public override string ToString() => ServiceId;
- }
-}
diff --git a/Emby.Dlna/Common/ServiceAction.cs b/Emby.Dlna/Common/ServiceAction.cs
deleted file mode 100644
index 02b81a0aa..000000000
--- a/Emby.Dlna/Common/ServiceAction.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-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>();
- }
-
- /// <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() => Name;
- }
-}
diff --git a/Emby.Dlna/Common/StateVariable.cs b/Emby.Dlna/Common/StateVariable.cs
deleted file mode 100644
index fd733e085..000000000
--- a/Emby.Dlna/Common/StateVariable.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Emby.Dlna.Common
-{
- /// <summary>
- /// Defines the <see cref="StateVariable" />.
- /// </summary>
- public class StateVariable
- {
- /// <summary>
- /// Gets or sets the name of the state variable.
- /// </summary>
- public string Name { get; set; } = string.Empty;
-
- /// <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; }
-
- /// <summary>
- /// Gets or sets the allowed values range.
- /// </summary>
- public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>();
-
- /// <inheritdoc />
- public override string ToString() => Name;
- }
-}
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
deleted file mode 100644
index f233468de..000000000
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-#pragma warning disable CS1591
-
-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;
- EnableServer = false;
- BlastAliveMessages = true;
- SendOnlyMatchedHost = true;
- ClientDiscoveryIntervalSeconds = 60;
- AliveMessageIntervalSeconds = 180;
- }
-
- /// <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; }
-
- /// <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; }
-
- /// <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/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs
deleted file mode 100644
index 3ca43052a..000000000
--- a/Emby.Dlna/ConfigurationExtension.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-
-using Emby.Dlna.Configuration;
-using MediaBrowser.Common.Configuration;
-
-namespace Emby.Dlna
-{
- public static class ConfigurationExtension
- {
- public static DlnaOptions GetDlnaConfiguration(this IConfigurationManager manager)
- {
- return manager.GetConfiguration<DlnaOptions>("dlna");
- }
- }
-}
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
deleted file mode 100644
index 916044a0c..000000000
--- a/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Net.Http;
-using System.Threading.Tasks;
-using Emby.Dlna.Service;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
-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,
- ILogger<ConnectionManagerService> logger,
- IHttpClientFactory httpClientFactory)
- : base(logger, httpClientFactory)
- {
- _dlna = dlna;
- _config = config;
- }
-
- /// <inheritdoc />
- public string GetServiceXml()
- {
- return ConnectionManagerXmlBuilder.GetXml();
- }
-
- /// <inheritdoc />
- public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
- {
- var profile = _dlna.GetProfile(request.Headers) ??
- _dlna.GetDefaultProfile();
-
- return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
- }
- }
-}
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
deleted file mode 100644
index db1190ae7..000000000
--- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using Emby.Dlna.Common;
-using Emby.Dlna.Service;
-
-namespace Emby.Dlna.ConnectionManager
-{
- /// <summary>
- /// Defines the <see cref="ConnectionManagerXmlBuilder" />.
- /// </summary>
- public static class ConnectionManagerXmlBuilder
- {
- /// <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(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()
- {
- return new StateVariable[]
- {
- new StateVariable
- {
- Name = "SourceProtocolInfo",
- DataType = "string",
- SendsEvents = true
- },
-
- new StateVariable
- {
- Name = "SinkProtocolInfo",
- DataType = "string",
- SendsEvents = true
- },
-
- new StateVariable
- {
- Name = "CurrentConnectionIDs",
- DataType = "string",
- SendsEvents = true
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionStatus",
- DataType = "string",
- SendsEvents = false,
-
- AllowedValues = new[]
- {
- "OK",
- "ContentFormatMismatch",
- "InsufficientBandwidth",
- "UnreliableChannel",
- "Unknown"
- }
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionManager",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_Direction",
- DataType = "string",
- SendsEvents = false,
-
- AllowedValues = new[]
- {
- "Output",
- "Input"
- }
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_ProtocolInfo",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_ConnectionID",
- DataType = "ui4",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_AVTransportID",
- DataType = "ui4",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_RcsID",
- DataType = "ui4",
- SendsEvents = false
- }
- };
- }
- }
-}
diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs
deleted file mode 100644
index 1a1790ee6..000000000
--- a/Emby.Dlna/ConnectionManager/ControlHandler.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Xml;
-using Emby.Dlna.Service;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.Dlna;
-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)
- {
- _profile = profile;
- }
-
- /// <inheritdoc />
- protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
- {
- if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
- {
- HandleGetProtocolInfo(xmlWriter);
- return;
- }
-
- 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);
- xmlWriter.WriteElementString("Sink", string.Empty);
- }
- }
-}
diff --git a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
deleted file mode 100644
index 542c7bfb4..000000000
--- a/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
+++ /dev/null
@@ -1,234 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using Emby.Dlna.Common;
-
-namespace Emby.Dlna.ConnectionManager
-{
- /// <summary>
- /// Defines the <see cref="ServiceActionListBuilder" />.
- /// </summary>
- public static class ServiceActionListBuilder
- {
- /// <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>
- {
- GetCurrentConnectionInfo(),
- GetProtocolInfo(),
- GetCurrentConnectionIDs(),
- ConnectionComplete(),
- PrepareForConnection()
- };
-
- return list;
- }
-
- /// <summary>
- /// Returns the action details for "PrepareForConnection".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction PrepareForConnection()
- {
- var action = new ServiceAction
- {
- Name = "PrepareForConnection"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RemoteProtocolInfo",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ProtocolInfo"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "PeerConnectionManager",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ConnectionManager"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "PeerConnectionID",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Direction",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Direction"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ConnectionID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "AVTransportID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_AVTransportID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RcsID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_RcsID"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetCurrentConnectionInfo".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetCurrentConnectionInfo()
- {
- var action = new ServiceAction
- {
- Name = "GetCurrentConnectionInfo"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ConnectionID",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RcsID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_RcsID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "AVTransportID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_AVTransportID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ProtocolInfo",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_ProtocolInfo"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "PeerConnectionManager",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_ConnectionManager"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "PeerConnectionID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Direction",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Direction"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Status",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_ConnectionStatus"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetProtocolInfo".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetProtocolInfo()
- {
- var action = new ServiceAction
- {
- Name = "GetProtocolInfo"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Source",
- Direction = "out",
- RelatedStateVariable = "SourceProtocolInfo"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Sink",
- Direction = "out",
- RelatedStateVariable = "SinkProtocolInfo"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetCurrentConnectionIDs".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetCurrentConnectionIDs()
- {
- var action = new ServiceAction
- {
- Name = "GetCurrentConnectionIDs"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ConnectionIDs",
- Direction = "out",
- RelatedStateVariable = "CurrentConnectionIDs"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "ConnectionComplete".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction ConnectionComplete()
- {
- var action = new ServiceAction
- {
- Name = "ConnectionComplete"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ConnectionID",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
- });
-
- return action;
- }
- }
-}
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
deleted file mode 100644
index 389e971a6..000000000
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
+++ /dev/null
@@ -1,173 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Linq;
-using System.Net.Http;
-using System.Threading.Tasks;
-using Emby.Dlna.Service;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.TV;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Globalization;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.ContentDirectory
-{
- /// <summary>
- /// Defines the <see cref="ContentDirectoryService" />.
- /// </summary>
- public class ContentDirectoryService : BaseService, IContentDirectory
- {
- private readonly ILibraryManager _libraryManager;
- private readonly IImageProcessor _imageProcessor;
- private readonly IUserDataManager _userDataManager;
- private readonly IDlnaManager _dlna;
- private readonly IServerConfigurationManager _config;
- private readonly IUserManager _userManager;
- private readonly ILocalizationManager _localization;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IUserViewManager _userViewManager;
- 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,
- IImageProcessor imageProcessor,
- ILibraryManager libraryManager,
- IServerConfigurationManager config,
- IUserManager userManager,
- ILogger<ContentDirectoryService> logger,
- IHttpClientFactory httpClient,
- ILocalizationManager localization,
- IMediaSourceManager mediaSourceManager,
- IUserViewManager userViewManager,
- IMediaEncoder mediaEncoder,
- ITVSeriesManager tvSeriesManager)
- : base(logger, httpClient)
- {
- _dlna = dlna;
- _userDataManager = userDataManager;
- _imageProcessor = imageProcessor;
- _libraryManager = libraryManager;
- _config = config;
- _userManager = userManager;
- _localization = localization;
- _mediaSourceManager = mediaSourceManager;
- _userViewManager = userViewManager;
- _mediaEncoder = mediaEncoder;
- _tvSeriesManager = tvSeriesManager;
- }
-
- /// <summary>
- /// Gets the system id. (A unique id which changes on when our definition changes.)
- /// </summary>
- private static int SystemUpdateId
- {
- get
- {
- var now = DateTime.UtcNow;
-
- return now.Year + now.DayOfYear + now.Hour;
- }
- }
-
- /// <inheritdoc />
- public string GetServiceXml()
- {
- return ContentDirectoryXmlBuilder.GetXml();
- }
-
- /// <inheritdoc />
- public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
- {
- ArgumentNullException.ThrowIfNull(request);
-
- var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
-
- var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
-
- var user = GetUser(profile);
-
- return new ControlHandler(
- Logger,
- _libraryManager,
- profile,
- serverAddress,
- null,
- _imageProcessor,
- _userDataManager,
- user,
- SystemUpdateId,
- _config,
- _localization,
- _mediaSourceManager,
- _userViewManager,
- _mediaEncoder,
- _tvSeriesManager)
- .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))
- {
- var user = _userManager.GetUserById(Guid.Parse(profile.UserId));
-
- if (user is not null)
- {
- return user;
- }
- }
-
- var userId = _config.GetDlnaConfiguration().DefaultUserId;
-
- if (!string.IsNullOrEmpty(userId))
- {
- var user = _userManager.GetUserById(Guid.Parse(userId));
-
- if (user is not null)
- {
- return user;
- }
- }
-
- foreach (var user in _userManager.Users)
- {
- if (user.HasPermission(PermissionKind.IsAdministrator))
- {
- return user;
- }
- }
-
- return _userManager.Users.FirstOrDefault();
- }
- }
-}
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
deleted file mode 100644
index 9af28aa7c..000000000
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
+++ /dev/null
@@ -1,159 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using Emby.Dlna.Common;
-using Emby.Dlna.Service;
-
-namespace Emby.Dlna.ContentDirectory
-{
- /// <summary>
- /// Defines the <see cref="ContentDirectoryXmlBuilder" />.
- /// </summary>
- public static class ContentDirectoryXmlBuilder
- {
- /// <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(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()
- {
- return new StateVariable[]
- {
- new StateVariable
- {
- Name = "A_ARG_TYPE_Filter",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_SortCriteria",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_Index",
- DataType = "ui4",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_Count",
- DataType = "ui4",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_UpdateID",
- DataType = "ui4",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "SearchCapabilities",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "SortCapabilities",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "SystemUpdateID",
- DataType = "ui4",
- SendsEvents = true
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_SearchCriteria",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_Result",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_ObjectID",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_BrowseFlag",
- DataType = "string",
- SendsEvents = false,
-
- AllowedValues = new[]
- {
- "BrowseMetadata",
- "BrowseDirectChildren"
- }
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_BrowseLetter",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_CategoryType",
- DataType = "ui4",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_RID",
- DataType = "ui4",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_PosSec",
- DataType = "ui4",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_Featurelist",
- DataType = "string",
- SendsEvents = false
- }
- };
- }
- }
-}
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
deleted file mode 100644
index abd594a3a..000000000
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ /dev/null
@@ -1,1262 +0,0 @@
-#nullable disable
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Xml;
-using Emby.Dlna.Didl;
-using Emby.Dlna.Service;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.TV;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-using Microsoft.Extensions.Logging;
-using Genre = MediaBrowser.Controller.Entities.Genre;
-
-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/";
- private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
- private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
- private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
-
- private readonly ILibraryManager _libraryManager;
- private readonly IUserDataManager _userDataManager;
- private readonly User _user;
- private readonly IUserViewManager _userViewManager;
- private readonly ITVSeriesManager _tvSeriesManager;
-
- private readonly int _systemUpdateId;
-
- private readonly DidlBuilder _didlBuilder;
-
- 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,
- DeviceProfile profile,
- string serverAddress,
- string accessToken,
- IImageProcessor imageProcessor,
- IUserDataManager userDataManager,
- User user,
- int systemUpdateId,
- IServerConfigurationManager config,
- ILocalizationManager localization,
- IMediaSourceManager mediaSourceManager,
- IUserViewManager userViewManager,
- IMediaEncoder mediaEncoder,
- ITVSeriesManager tvSeriesManager)
- : base(config, logger)
- {
- _libraryManager = libraryManager;
- _userDataManager = userDataManager;
- _user = user;
- _systemUpdateId = systemUpdateId;
- _userViewManager = userViewManager;
- _tvSeriesManager = tvSeriesManager;
- _profile = profile;
-
- _didlBuilder = new DidlBuilder(
- profile,
- user,
- imageProcessor,
- serverAddress,
- accessToken,
- userDataManager,
- localization,
- mediaSourceManager,
- Logger,
- mediaEncoder,
- libraryManager);
- }
-
- /// <inheritdoc />
- protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
- {
- ArgumentNullException.ThrowIfNull(xmlWriter);
-
- ArgumentNullException.ThrowIfNull(methodParams);
-
- const string DeviceId = "test";
-
- if (string.Equals(methodName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase))
- {
- HandleGetSearchCapabilities(xmlWriter);
- return;
- }
-
- if (string.Equals(methodName, "GetSortCapabilities", StringComparison.OrdinalIgnoreCase))
- {
- HandleGetSortCapabilities(xmlWriter);
- return;
- }
-
- if (string.Equals(methodName, "GetSortExtensionCapabilities", StringComparison.OrdinalIgnoreCase))
- {
- HandleGetSortExtensionCapabilities(xmlWriter);
- return;
- }
-
- if (string.Equals(methodName, "GetSystemUpdateID", StringComparison.OrdinalIgnoreCase))
- {
- HandleGetSystemUpdateID(xmlWriter);
- return;
- }
-
- if (string.Equals(methodName, "Browse", StringComparison.OrdinalIgnoreCase))
- {
- HandleBrowse(xmlWriter, methodParams, DeviceId);
- return;
- }
-
- if (string.Equals(methodName, "X_GetFeatureList", StringComparison.OrdinalIgnoreCase))
- {
- HandleXGetFeatureList(xmlWriter);
- return;
- }
-
- if (string.Equals(methodName, "GetFeatureList", StringComparison.OrdinalIgnoreCase))
- {
- HandleGetFeatureList(xmlWriter);
- return;
- }
-
- if (string.Equals(methodName, "X_SetBookmark", StringComparison.OrdinalIgnoreCase))
- {
- HandleXSetBookmark(methodParams);
- return;
- }
-
- if (string.Equals(methodName, "Search", StringComparison.OrdinalIgnoreCase))
- {
- HandleSearch(xmlWriter, methodParams, DeviceId);
- return;
- }
-
- if (string.Equals(methodName, "X_BrowseByLetter", StringComparison.OrdinalIgnoreCase))
- {
- HandleXBrowseByLetter(xmlWriter, methodParams, DeviceId);
- return;
- }
-
- throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
- }
-
- /// <summary>
- /// Adds a "XSetBookmark" element to the xml document.
- /// </summary>
- /// <param name="sparams">The method parameters.</param>
- private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
- {
- var id = sparams["ObjectID"];
-
- var serverItem = GetItemFromObjectId(id);
-
- var item = serverItem.Item;
-
- var newbookmark = int.Parse(sparams["PosSecond"], CultureInfo.InvariantCulture);
-
- var userdata = _userDataManager.GetUserData(_user, item);
-
- userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
-
- _userDataManager.SaveUserData(
- _user,
- item,
- userdata,
- UserDataSaveReason.TogglePlayed,
- CancellationToken.None);
- }
-
- /// <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");
- }
-
- /// <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");
- }
-
- /// <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));
- }
-
- /// <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());
- }
-
- /// <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);
-
- /// <summary>
- /// Builds a static feature list.
- /// </summary>
- /// <returns>The xml feature list.</returns>
- private static string WriteFeatureListXml()
- {
- return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
- + "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"
- + "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
- + "<container id=\"0\" type=\"object.item.imageItem\"/>"
- + "<container id=\"0\" type=\"object.item.audioItem\"/>"
- + "<container id=\"0\" type=\"object.item.videoItem\"/>"
- + "</Feature>"
- + "</Features>";
- }
-
- /// <summary>
- /// Builds the "Browse" xml response.
- /// </summary>
- /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
- /// <param name="sparams">The method parameters.</param>
- /// <param name="deviceId">The device Id to use.</param>
- private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
- {
- var id = sparams["ObjectID"];
- var flag = sparams["BrowseFlag"];
- var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
- var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
-
- var provided = 0;
-
- // Default to null instead of 0
- // Upnp inspector sends 0 as requestedCount when it wants everything
- int? requestedCount = null;
- int? start = 0;
-
- if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out var requestedVal) && requestedVal > 0)
- {
- requestedCount = requestedVal;
- }
-
- if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out var startVal) && startVal > 0)
- {
- start = startVal;
- }
-
- int totalCount;
-
- var settings = new XmlWriterSettings
- {
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
-
- using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
-
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
-
- DidlBuilder.WriteXmlRootAttributes(_profile, writer);
-
- var serverItem = GetItemFromObjectId(id);
- var item = serverItem.Item;
-
- if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
- {
- totalCount = 1;
-
- if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
- {
- var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
-
- _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
- }
-
- provided++;
- }
- else
- {
- var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
- totalCount = childrenResult.TotalRecordCount;
-
- provided = childrenResult.Items.Count;
-
- foreach (var i in childrenResult.Items)
- {
- var childItem = i.Item;
- var displayStubType = i.StubType;
-
- if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
- {
- var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0)
- .TotalRecordCount;
-
- _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter);
- }
- }
- }
-
- writer.WriteFullEndElement();
- writer.Flush();
- xmlWriter.WriteElementString("Result", builder.ToString());
- }
-
- xmlWriter.WriteElementString("NumberReturned", provided.ToString(CultureInfo.InvariantCulture));
- xmlWriter.WriteElementString("TotalMatches", totalCount.ToString(CultureInfo.InvariantCulture));
- 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 method parameters.</param>
- /// <param name="deviceId">The device id.</param>
- private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<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 method parameters.</param>
- /// <param name="deviceId">The deviceId<see cref="string"/>.</param>
- private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
- {
- var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty));
- var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
- var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
-
- // sort example: dc:title, dc:date
-
- // Default to null instead of 0
- // Upnp inspector sends 0 as requestedCount when it wants everything
- int? requestedCount = null;
- int? start = 0;
-
- if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out var requestedVal) && requestedVal > 0)
- {
- requestedCount = requestedVal;
- }
-
- if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out var startVal) && startVal > 0)
- {
- start = startVal;
- }
-
- QueryResult<BaseItem> childrenResult;
- var settings = new XmlWriterSettings
- {
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
-
- using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
-
- DidlBuilder.WriteXmlRootAttributes(_profile, writer);
-
- var serverItem = GetItemFromObjectId(sparams["ContainerID"]);
-
- var item = serverItem.Item;
-
- childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount);
- foreach (var i in childrenResult.Items)
- {
- if (i.IsDisplayedAsFolder)
- {
- var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0)
- .TotalRecordCount;
-
- _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter);
- }
- }
-
- writer.WriteFullEndElement();
- writer.Flush();
- xmlWriter.WriteElementString("Result", builder.ToString());
- }
-
- xmlWriter.WriteElementString("NumberReturned", childrenResult.Items.Count.ToString(CultureInfo.InvariantCulture));
- xmlWriter.WriteElementString("TotalMatches", childrenResult.TotalRecordCount.ToString(CultureInfo.InvariantCulture));
- xmlWriter.WriteElementString("UpdateID", _systemUpdateId.ToString(CultureInfo.InvariantCulture));
- }
-
- /// <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;
-
- string[] mediaTypes = Array.Empty<string>();
- bool? isFolder = null;
-
- switch (search.SearchType)
- {
- case SearchType.Audio:
- mediaTypes = new[] { MediaType.Audio };
- isFolder = false;
- break;
- case SearchType.Video:
- mediaTypes = new[] { MediaType.Video };
- isFolder = false;
- break;
- case SearchType.Image:
- mediaTypes = new[] { MediaType.Photo };
- isFolder = false;
- break;
- case SearchType.Playlist:
- case SearchType.MusicAlbum:
- isFolder = true;
- break;
- }
-
- return folder.GetItems(new InternalItemsQuery
- {
- Limit = limit,
- StartIndex = startIndex,
- OrderBy = GetOrderBy(sort, folder.IsPreSorted),
- User = user,
- Recursive = true,
- IsMissing = false,
- ExcludeItemTypes = new[] { BaseItemKind.Book },
- IsFolder = isFolder,
- MediaTypes = mediaTypes,
- 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)
- {
- switch (item)
- {
- case MusicGenre:
- return GetMusicGenreItems(item, user, sort, startIndex, limit);
- case MusicArtist:
- return GetMusicArtistItems(item, user, sort, startIndex, limit);
- case Genre:
- return GetGenreItems(item, user, sort, startIndex, limit);
- }
-
- if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder)
- {
- var collectionType = collectionFolder.CollectionType;
- if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
- }
-
- if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
- }
-
- if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetTvFolders(item, user, stubType, sort, startIndex, limit);
- }
-
- if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetFolders(user, startIndex, limit);
- }
-
- if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetLiveTvChannels(user, sort, startIndex, limit);
- }
- }
-
- if (stubType.HasValue && stubType.Value != StubType.Folder)
- {
- // TODO should this be doing something?
- return new QueryResult<ServerItem>();
- }
-
- var folder = (Folder)item;
-
- var query = new InternalItemsQuery(user)
- {
- Limit = limit,
- StartIndex = startIndex,
- IsVirtualItem = false,
- ExcludeItemTypes = new[] { BaseItemKind.Book },
- IsPlaceHolder = false,
- DtoOptions = GetDtoOptions(),
- OrderBy = GetOrderBy(sort, folder.IsPreSorted)
- };
-
- var queryResult = folder.GetItems(query);
-
- return ToResult(startIndex, 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)
- {
- StartIndex = startIndex,
- Limit = limit,
- IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
- OrderBy = GetOrderBy(sort, false)
- };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(startIndex, 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)
- {
- StartIndex = startIndex,
- Limit = limit,
- OrderBy = GetOrderBy(sort, false)
- };
-
- switch (stubType)
- {
- case StubType.Latest:
- return GetLatest(item, query, BaseItemKind.Audio);
- case StubType.Playlists:
- return GetMusicPlaylists(query);
- case StubType.Albums:
- return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum);
- case StubType.Artists:
- return GetMusicArtists(item, query);
- case StubType.AlbumArtists:
- return GetMusicAlbumArtists(item, query);
- case StubType.FavoriteAlbums:
- return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true);
- case StubType.FavoriteArtists:
- return GetFavoriteArtists(item, query);
- case StubType.FavoriteSongs:
- return GetChildrenOfItem(item, query, BaseItemKind.Audio, true);
- case StubType.Songs:
- return GetChildrenOfItem(item, query, BaseItemKind.Audio);
- case StubType.Genres:
- return GetMusicGenres(item, query);
- }
-
- var serverItems = new ServerItem[]
- {
- new(item, StubType.Latest),
- new(item, StubType.Playlists),
- new(item, StubType.Albums),
- new(item, StubType.AlbumArtists),
- new(item, StubType.Artists),
- new(item, StubType.Songs),
- new(item, StubType.Genres),
- new(item, StubType.FavoriteArtists),
- new(item, StubType.FavoriteAlbums),
- new(item, StubType.FavoriteSongs)
- };
-
- if (limit < serverItems.Length)
- {
- serverItems = serverItems[..limit.Value];
- }
-
- return new QueryResult<ServerItem>(
- startIndex,
- serverItems.Length,
- serverItems);
- }
-
- /// <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)
- {
- StartIndex = startIndex,
- Limit = limit,
- OrderBy = GetOrderBy(sort, false)
- };
-
- switch (stubType)
- {
- case StubType.ContinueWatching:
- return GetMovieContinueWatching(item, query);
- case StubType.Latest:
- return GetLatest(item, query, BaseItemKind.Movie);
- case StubType.Movies:
- return GetChildrenOfItem(item, query, BaseItemKind.Movie);
- case StubType.Collections:
- return GetMovieCollections(query);
- case StubType.Favorites:
- return GetChildrenOfItem(item, query, BaseItemKind.Movie, true);
- case StubType.Genres:
- return GetGenres(item, query);
- }
-
- var array = new ServerItem[]
- {
- new(item, StubType.ContinueWatching),
- new(item, StubType.Latest),
- new(item, StubType.Movies),
- new(item, StubType.Collections),
- new(item, StubType.Favorites),
- new(item, StubType.Genres)
- };
-
- if (limit < array.Length)
- {
- array = array[..limit.Value];
- }
-
- return new QueryResult<ServerItem>(
- startIndex,
- array.Length,
- array);
- }
-
- /// <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);
- var totalRecordCount = folders.Count;
- // Handle paging
- var items = folders
- .OrderBy(i => i.SortName)
- .Skip(startIndex ?? 0)
- .Take(limit ?? int.MaxValue)
- .Select(i => new ServerItem(i, StubType.Folder))
- .ToArray();
-
- return new QueryResult<ServerItem>(
- startIndex,
- totalRecordCount,
- items);
- }
-
- /// <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)
- {
- StartIndex = startIndex,
- Limit = limit,
- OrderBy = GetOrderBy(sort, false)
- };
-
- switch (stubType)
- {
- case StubType.ContinueWatching:
- return GetMovieContinueWatching(item, query);
- case StubType.NextUp:
- return GetNextUp(item, query);
- case StubType.Latest:
- return GetLatest(item, query, BaseItemKind.Episode);
- case StubType.Series:
- return GetChildrenOfItem(item, query, BaseItemKind.Series);
- case StubType.FavoriteSeries:
- return GetChildrenOfItem(item, query, BaseItemKind.Series, true);
- case StubType.FavoriteEpisodes:
- return GetChildrenOfItem(item, query, BaseItemKind.Episode, true);
- case StubType.Genres:
- return GetGenres(item, query);
- }
-
- var serverItems = new ServerItem[]
- {
- new(item, StubType.ContinueWatching),
- new(item, StubType.NextUp),
- new(item, StubType.Latest),
- new(item, StubType.Series),
- new(item, StubType.FavoriteSeries),
- new(item, StubType.FavoriteEpisodes),
- new(item, StubType.Genres)
- };
-
- if (limit < serverItems.Length)
- {
- serverItems = serverItems[..limit.Value];
- }
-
- return new QueryResult<ServerItem>(
- startIndex,
- serverItems.Length,
- serverItems);
- }
-
- /// <summary>
- /// Returns the Movies that are part watched that meet 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> GetMovieContinueWatching(BaseItem parent, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
-
- query.OrderBy = new[]
- {
- (ItemSortBy.DatePlayed, SortOrder.Descending),
- (ItemSortBy.SortName, SortOrder.Ascending)
- };
-
- query.IsResumable = true;
- query.Limit ??= 10;
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(query.StartIndex, result);
- }
-
- /// <summary>
- /// Returns the Movie collections meeting the criteria.
- /// </summary>
- /// <param name="query">The see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMovieCollections(InternalItemsQuery query)
- {
- query.Recursive = true;
- query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(query.StartIndex, result);
- }
-
- /// <summary>
- /// Returns the children that meet the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <param name="itemType">The item type.</param>
- /// <param name="isFavorite">A value indicating whether to only fetch favorite items.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.IsFavorite = isFavorite;
- query.IncludeItemTypes = new[] { itemType };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(query.StartIndex, result);
- }
-
- /// <summary>
- /// Returns the genres meeting the criteria.
- /// The GetGenres.
- /// </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> GetGenres(BaseItem parent, InternalItemsQuery query)
- {
- // Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
- query.AncestorIds = new[] { parent.Id };
- var genresResult = _libraryManager.GetGenres(query);
-
- return ToResult(query.StartIndex, genresResult);
- }
-
- /// <summary>
- /// Returns the music genres 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> GetMusicGenres(BaseItem parent, InternalItemsQuery query)
- {
- // Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
- query.AncestorIds = new[] { parent.Id };
- var genresResult = _libraryManager.GetMusicGenres(query);
-
- return ToResult(query.StartIndex, genresResult);
- }
-
- /// <summary>
- /// Returns the music albums by artist that meet 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> GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query)
- {
- // Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
- query.AncestorIds = new[] { parent.Id };
- var artists = _libraryManager.GetAlbumArtists(query);
-
- return ToResult(query.StartIndex, artists);
- }
-
- /// <summary>
- /// Returns the music artists 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> GetMusicArtists(BaseItem parent, InternalItemsQuery query)
- {
- // Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
- query.AncestorIds = new[] { parent.Id };
- var artists = _libraryManager.GetArtists(query);
- return ToResult(query.StartIndex, artists);
- }
-
- /// <summary>
- /// Returns the artists tagged as favourite that meet 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> GetFavoriteArtists(BaseItem parent, InternalItemsQuery query)
- {
- // Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
- query.AncestorIds = new[] { parent.Id };
- query.IsFavorite = true;
- var artists = _libraryManager.GetArtists(query);
- return ToResult(query.StartIndex, artists);
- }
-
- /// <summary>
- /// Returns the music playlists meeting the criteria.
- /// </summary>
- /// <param name="query">The query<see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicPlaylists(InternalItemsQuery query)
- {
- query.Parent = null;
- query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
- query.Recursive = true;
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(query.StartIndex, result);
- }
-
- /// <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)>();
-
- var result = _tvSeriesManager.GetNextUp(
- new NextUpQuery
- {
- Limit = query.Limit,
- StartIndex = query.StartIndex,
- // User cannot be null here as the caller has set it
- UserId = query.User!.Id
- },
- new[] { parent },
- query.DtoOptions);
-
- return ToResult(query.StartIndex, result);
- }
-
- /// <summary>
- /// Returns the latest items of [itemType] meeting the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <param name="itemType">The item type.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
- {
- query.OrderBy = Array.Empty<(string, SortOrder)>();
-
- var items = _userViewManager.GetLatestItems(
- new LatestItemsQuery
- {
- // User cannot be null here as the caller has set it
- UserId = query.User!.Id,
- Limit = query.Limit ?? 50,
- IncludeItemTypes = new[] { itemType },
- ParentId = parent?.Id ?? Guid.Empty,
- GroupItems = true
- },
- query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i is not null).ToArray();
-
- return ToResult(query.StartIndex, items);
- }
-
- /// <summary>
- /// Returns music artist items that meet the criteria.
- /// </summary>
- /// <param name="item">The <see cref="BaseItem"/>.</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, User user, SortCriteria sort, int? startIndex, int? limit)
- {
- var query = new InternalItemsQuery(user)
- {
- Recursive = true,
- ArtistIds = new[] { item.Id },
- IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
- Limit = limit,
- StartIndex = startIndex,
- DtoOptions = GetDtoOptions(),
- OrderBy = GetOrderBy(sort, false)
- };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(startIndex, result);
- }
-
- /// <summary>
- /// Returns the genre items meeting the criteria.
- /// </summary>
- /// <param name="item">The <see cref="BaseItem"/>.</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, User user, SortCriteria sort, int? startIndex, int? limit)
- {
- var query = new InternalItemsQuery(user)
- {
- Recursive = true,
- GenreIds = new[] { item.Id },
- IncludeItemTypes = new[]
- {
- BaseItemKind.Movie,
- BaseItemKind.Series
- },
- Limit = limit,
- StartIndex = startIndex,
- DtoOptions = GetDtoOptions(),
- OrderBy = GetOrderBy(sort, false)
- };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(startIndex, result);
- }
-
- /// <summary>
- /// Returns the music genre items meeting the criteria.
- /// </summary>
- /// <param name="item">The <see cref="BaseItem"/>.</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, User user, SortCriteria sort, int? startIndex, int? limit)
- {
- var query = new InternalItemsQuery(user)
- {
- Recursive = true,
- GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
- Limit = limit,
- StartIndex = startIndex,
- DtoOptions = GetDtoOptions(),
- OrderBy = GetOrderBy(sort, false)
- };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(startIndex, result);
- }
-
- /// <summary>
- /// Converts <see cref="IReadOnlyCollection{BaseItem}"/> into a <see cref="QueryResult{ServerItem}"/>.
- /// </summary>
- /// <param name="startIndex">The start index.</param>
- /// <param name="result">An array of <see cref="BaseItem"/>.</param>
- /// <returns>A <see cref="QueryResult{ServerItem}"/>.</returns>
- private static QueryResult<ServerItem> ToResult(int? startIndex, IReadOnlyCollection<BaseItem> result)
- {
- var serverItems = result
- .Select(i => new ServerItem(i, null))
- .ToArray();
-
- return new QueryResult<ServerItem>(
- startIndex,
- result.Count,
- serverItems);
- }
-
- /// <summary>
- /// Converts a <see cref="QueryResult{BaseItem}"/> to a <see cref="QueryResult{ServerItem}"/>.
- /// </summary>
- /// <param name="startIndex">The index the result started at.</param>
- /// <param name="result">A <see cref="QueryResult{BaseItem}"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private static QueryResult<ServerItem> ToResult(int? startIndex, QueryResult<BaseItem> result)
- {
- var length = result.Items.Count;
- var serverItems = new ServerItem[length];
- for (var i = 0; i < length; i++)
- {
- serverItems[i] = new ServerItem(result.Items[i], null);
- }
-
- return new QueryResult<ServerItem>(
- startIndex,
- result.TotalRecordCount,
- serverItems);
- }
-
- /// <summary>
- /// Converts a query result to a <see cref="QueryResult{ServerItem}"/>.
- /// </summary>
- /// <param name="startIndex">The start index.</param>
- /// <param name="result">A <see cref="QueryResult{BaseItem}"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private static QueryResult<ServerItem> ToResult(int? startIndex, QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result)
- {
- var length = result.Items.Count;
- var serverItems = new ServerItem[length];
- for (var i = 0; i < length; i++)
- {
- serverItems[i] = new ServerItem(result.Items[i].Item, null);
- }
-
- return new QueryResult<ServerItem>(
- startIndex,
- result.TotalRecordCount,
- serverItems);
- }
-
- /// <summary>
- /// Gets the sorting method on a query.
- /// </summary>
- /// <param name="sort">The <see cref="SortCriteria"/>.</param>
- /// <param name="isPreSorted">True if pre-sorted.</param>
- private static (string SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
- {
- return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
- }
-
- /// <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)
- ? new ServerItem(_libraryManager.GetUserRootFolder(), null)
- : 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;
-
- // After using PlayTo, MediaMonkey sends a request to the server trying to get item info
- const string ParamsSrch = "Params=";
- var paramsIndex = id.IndexOf(ParamsSrch, StringComparison.OrdinalIgnoreCase);
- if (paramsIndex != -1)
- {
- id = id[(paramsIndex + ParamsSrch.Length)..];
-
- var parts = id.Split(';');
- id = parts[23];
- }
-
- var dividerIndex = id.IndexOf('_', StringComparison.Ordinal);
- if (dividerIndex != -1 && Enum.TryParse<StubType>(id.AsSpan(0, dividerIndex), true, out var parsedStubType))
- {
- id = id[(dividerIndex + 1)..];
- stubType = parsedStubType;
- }
-
- if (Guid.TryParse(id, out var itemId))
- {
- var item = _libraryManager.GetItemById(itemId);
-
- return new ServerItem(item, stubType);
- }
-
- Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
-
- return new ServerItem(_libraryManager.GetUserRootFolder(), null);
- }
- }
-}
diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs
deleted file mode 100644
index df05fa966..000000000
--- a/Emby.Dlna/ContentDirectory/ServerItem.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-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>
- /// <param name="stubType">The stub type.</param>
- public ServerItem(BaseItem item, StubType? stubType)
- {
- Item = item;
-
- if (stubType.HasValue)
- {
- StubType = stubType;
- }
- else if (item is IItemByName and not Folder)
- {
- StubType = Dlna.ContentDirectory.StubType.Folder;
- }
- }
-
- /// <summary>
- /// Gets the underlying base item.
- /// </summary>
- public BaseItem Item { get; }
-
- /// <summary>
- /// Gets the DLNA item type.
- /// </summary>
- public StubType? StubType { get; }
- }
-}
diff --git a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
deleted file mode 100644
index 7e3db4651..000000000
--- a/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
+++ /dev/null
@@ -1,415 +0,0 @@
-using System.Collections.Generic;
-using Emby.Dlna.Common;
-
-namespace Emby.Dlna.ContentDirectory
-{
- /// <summary>
- /// Defines the <see cref="ServiceActionListBuilder" />.
- /// </summary>
- public static class ServiceActionListBuilder
- {
- /// <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[]
- {
- GetSearchCapabilitiesAction(),
- GetSortCapabilitiesAction(),
- GetGetSystemUpdateIDAction(),
- GetBrowseAction(),
- GetSearchAction(),
- GetX_GetFeatureListAction(),
- GetXSetBookmarkAction(),
- GetBrowseByLetterAction()
- };
- }
-
- /// <summary>
- /// Returns the action details for "GetSystemUpdateID".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetGetSystemUpdateIDAction()
- {
- var action = new ServiceAction
- {
- Name = "GetSystemUpdateID"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Id",
- Direction = "out",
- RelatedStateVariable = "SystemUpdateID"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetSearchCapabilities".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetSearchCapabilitiesAction()
- {
- var action = new ServiceAction
- {
- Name = "GetSearchCapabilities"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "SearchCaps",
- Direction = "out",
- RelatedStateVariable = "SearchCapabilities"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetSortCapabilities".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetSortCapabilitiesAction()
- {
- var action = new ServiceAction
- {
- Name = "GetSortCapabilities"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "SortCaps",
- Direction = "out",
- RelatedStateVariable = "SortCapabilities"
- });
-
- 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
- {
- Name = "X_GetFeatureList"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "FeatureList",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Featurelist"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "Search".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetSearchAction()
- {
- var action = new ServiceAction
- {
- Name = "Search"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ContainerID",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ObjectID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "SearchCriteria",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_SearchCriteria"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Filter",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Filter"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "StartingIndex",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Index"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RequestedCount",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "SortCriteria",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Result",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Result"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "NumberReturned",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "TotalMatches",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "UpdateID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_UpdateID"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "Browse".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetBrowseAction()
- {
- var action = new ServiceAction
- {
- Name = "Browse"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ObjectID",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ObjectID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "BrowseFlag",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_BrowseFlag"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Filter",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Filter"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "StartingIndex",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Index"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RequestedCount",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "SortCriteria",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Result",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Result"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "NumberReturned",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "TotalMatches",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "UpdateID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_UpdateID"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "X_BrowseByLetter".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetBrowseByLetterAction()
- {
- var action = new ServiceAction
- {
- Name = "X_BrowseByLetter"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ObjectID",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ObjectID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "BrowseFlag",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_BrowseFlag"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Filter",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Filter"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "StartingLetter",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_BrowseLetter"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RequestedCount",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "SortCriteria",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Result",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Result"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "NumberReturned",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "TotalMatches",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Count"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "UpdateID",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_UpdateID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "StartingIndex",
- Direction = "out",
- RelatedStateVariable = "A_ARG_TYPE_Index"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "X_SetBookmark".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetXSetBookmarkAction()
- {
- var action = new ServiceAction
- {
- Name = "X_SetBookmark"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "CategoryType",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_CategoryType"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RID",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_RID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ObjectID",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_ObjectID"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "PosSecond",
- Direction = "in",
- RelatedStateVariable = "A_ARG_TYPE_PosSec"
- });
-
- return action;
- }
- }
-}
diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs
deleted file mode 100644
index 187dc1d75..000000000
--- a/Emby.Dlna/ContentDirectory/StubType.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Dlna.ContentDirectory
-{
- /// <summary>
- /// Defines the DLNA item types.
- /// </summary>
- public enum StubType
- {
- Folder = 0,
- Latest = 2,
- Playlists = 3,
- Albums = 4,
- AlbumArtists = 5,
- Artists = 6,
- Songs = 7,
- Genres = 8,
- FavoriteSongs = 9,
- FavoriteArtists = 10,
- FavoriteAlbums = 11,
- ContinueWatching = 12,
- Movies = 13,
- Collections = 14,
- Favorites = 15,
- NextUp = 16,
- Series = 17,
- FavoriteSeries = 18,
- FavoriteEpisodes = 19
- }
-}
diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs
deleted file mode 100644
index 8ee6325e9..000000000
--- a/Emby.Dlna/ControlRequest.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.IO;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Dlna
-{
- public class ControlRequest
- {
- public ControlRequest(IHeaderDictionary headers)
- {
- Headers = headers;
- }
-
- public IHeaderDictionary Headers { get; }
-
- public Stream InputXml { get; set; }
-
- public string TargetServerUuId { get; set; }
-
- public string RequestedUrl { get; set; }
- }
-}
diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs
deleted file mode 100644
index 8b0958842..000000000
--- a/Emby.Dlna/ControlResponse.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace Emby.Dlna
-{
- public class ControlResponse
- {
- public ControlResponse(string xml, bool isSuccessful)
- {
- Headers = new Dictionary<string, string>();
- Xml = xml;
- IsSuccessful = isSuccessful;
- }
-
- public IDictionary<string, string> Headers { get; }
-
- public string Xml { get; set; }
-
- public bool IsSuccessful { get; set; }
-
- /// <inheritdoc />
- public override string ToString()
- {
- return Xml;
- }
- }
-}
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
deleted file mode 100644
index 5ed982876..000000000
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ /dev/null
@@ -1,1266 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Xml;
-using Emby.Dlna.ContentDirectory;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Logging;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
-using Genre = MediaBrowser.Controller.Entities.Genre;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-using Season = MediaBrowser.Controller.Entities.TV.Season;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
-using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
-
-namespace Emby.Dlna.Didl
-{
- public class DidlBuilder
- {
- private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
- private const string NsDc = "http://purl.org/dc/elements/1.1/";
- private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
- private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
-
- private readonly DeviceProfile _profile;
- private readonly IImageProcessor _imageProcessor;
- private readonly string _serverAddress;
- private readonly string? _accessToken;
- private readonly User? _user;
- private readonly IUserDataManager _userDataManager;
- private readonly ILocalizationManager _localization;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly ILogger _logger;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly ILibraryManager _libraryManager;
-
- public DidlBuilder(
- DeviceProfile profile,
- User? user,
- IImageProcessor imageProcessor,
- string serverAddress,
- string? accessToken,
- IUserDataManager userDataManager,
- ILocalizationManager localization,
- IMediaSourceManager mediaSourceManager,
- ILogger logger,
- IMediaEncoder mediaEncoder,
- ILibraryManager libraryManager)
- {
- _profile = profile;
- _user = user;
- _imageProcessor = imageProcessor;
- _serverAddress = serverAddress;
- _accessToken = accessToken;
- _userDataManager = userDataManager;
- _localization = localization;
- _mediaSourceManager = mediaSourceManager;
- _logger = logger;
- _mediaEncoder = mediaEncoder;
- _libraryManager = libraryManager;
- }
-
- public static string NormalizeDlnaMediaUrl(string url)
- {
- return url + "&dlnaheaders=true";
- }
-
- public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo)
- {
- var settings = new XmlWriterSettings
- {
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
-
- using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
- {
- // If this using are changed to single lines, then write.Flush needs to be appended before the return.
- using (var writer = XmlWriter.Create(builder, settings))
- {
- // writer.WriteStartDocument();
-
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
-
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
- // didl.SetAttribute("xmlns:sec", NS_SEC);
-
- WriteXmlRootAttributes(_profile, writer);
-
- WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
-
- writer.WriteFullEndElement();
- // writer.WriteEndDocument();
- }
-
- return builder.ToString();
- }
- }
-
- public static void WriteXmlRootAttributes(DeviceProfile profile, XmlWriter writer)
- {
- foreach (var att in profile.XmlRootAttributes)
- {
- var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
- if (parts.Length == 2)
- {
- writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
- }
- else
- {
- writer.WriteAttributeString(att.Name, att.Value);
- }
- }
- }
-
- public void WriteItemElement(
- XmlWriter writer,
- BaseItem item,
- User? user,
- BaseItem? context,
- StubType? contextStubType,
- string deviceId,
- Filter filter,
- StreamInfo? streamInfo = null)
- {
- var clientId = GetClientId(item, null);
-
- writer.WriteStartElement(string.Empty, "item", NsDidl);
-
- writer.WriteAttributeString("restricted", "1");
- writer.WriteAttributeString("id", clientId);
-
- if (context is not null)
- {
- writer.WriteAttributeString("parentID", GetClientId(context, contextStubType));
- }
- else
- {
- var parent = item.DisplayParentId;
- if (!parent.Equals(default))
- {
- writer.WriteAttributeString("parentID", GetClientId(parent, null));
- }
- }
-
- AddGeneralProperties(item, null, context, writer, filter);
-
- AddSamsungBookmarkInfo(item, user, writer, streamInfo);
-
- // refID?
- // storeAttribute(itemNode, object, ClassProperties.REF_ID, false);
-
- if (item is IHasMediaSources)
- {
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
- {
- AddAudioResource(writer, item, deviceId, filter, streamInfo);
- }
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- AddVideoResource(writer, item, deviceId, filter, streamInfo);
- }
- }
-
- AddCover(item, null, writer);
- writer.WriteFullEndElement();
- }
-
- private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
- {
- if (streamInfo is null)
- {
- var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user);
-
- streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
- {
- ItemId = video.Id,
- MediaSources = sources.ToArray(),
- Profile = _profile,
- DeviceId = deviceId,
- MaxBitrate = _profile.MaxStreamingBitrate
- }) ?? throw new InvalidOperationException("No optimal video stream found");
- }
-
- var targetWidth = streamInfo.TargetWidth;
- var targetHeight = streamInfo.TargetHeight;
-
- var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
- _profile,
- streamInfo.Container,
- streamInfo.TargetVideoCodec.FirstOrDefault(),
- streamInfo.TargetAudioCodec.FirstOrDefault(),
- targetWidth,
- targetHeight,
- streamInfo.TargetVideoBitDepth,
- streamInfo.TargetVideoBitrate,
- streamInfo.TargetTimestamp,
- streamInfo.IsDirectStream,
- streamInfo.RunTimeTicks ?? 0,
- streamInfo.TargetVideoProfile,
- streamInfo.TargetVideoRangeType,
- streamInfo.TargetVideoLevel,
- streamInfo.TargetFramerate ?? 0,
- streamInfo.TargetPacketLength,
- streamInfo.TranscodeSeekInfo,
- streamInfo.IsTargetAnamorphic,
- streamInfo.IsTargetInterlaced,
- streamInfo.TargetRefFrames,
- streamInfo.TargetVideoStreamCount,
- streamInfo.TargetAudioStreamCount,
- streamInfo.TargetVideoCodecTag,
- streamInfo.IsTargetAVC);
-
- foreach (var contentFeature in contentFeatureList)
- {
- AddVideoResource(writer, filter, contentFeature, streamInfo);
- }
-
- var subtitleProfiles = streamInfo.GetSubtitleProfiles(_mediaEncoder, false, _serverAddress, _accessToken);
-
- foreach (var subtitle in subtitleProfiles)
- {
- if (subtitle.DeliveryMethod != SubtitleDeliveryMethod.External)
- {
- continue;
- }
-
- var subtitleAdded = AddSubtitleElement(writer, subtitle);
-
- if (subtitleAdded && _profile.EnableSingleSubtitleLimit)
- {
- break;
- }
- }
- }
-
- private bool AddSubtitleElement(XmlWriter writer, SubtitleStreamInfo info)
- {
- var subtitleProfile = _profile.SubtitleProfiles
- .FirstOrDefault(i => string.Equals(info.Format, i.Format, StringComparison.OrdinalIgnoreCase)
- && i.Method == SubtitleDeliveryMethod.External);
-
- if (subtitleProfile is null)
- {
- return false;
- }
-
- var subtitleMode = subtitleProfile.DidlMode;
-
- if (string.Equals(subtitleMode, "CaptionInfoEx", StringComparison.OrdinalIgnoreCase))
- {
- // <sec:CaptionInfoEx sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfoEx>
- // <sec:CaptionInfo sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfo>
-
- writer.WriteStartElement("sec", "CaptionInfoEx", null);
- writer.WriteAttributeString("sec", "type", null, info.Format.ToLowerInvariant());
-
- writer.WriteString(info.Url);
- writer.WriteFullEndElement();
- }
- else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase))
- {
- writer.WriteStartElement(string.Empty, "res", NsDidl);
-
- writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
-
- writer.WriteString(info.Url);
- writer.WriteFullEndElement();
- }
- else
- {
- writer.WriteStartElement(string.Empty, "res", NsDidl);
- var protocolInfo = string.Format(
- CultureInfo.InvariantCulture,
- "http-get:*:text/{0}:*",
- info.Format.ToLowerInvariant());
- writer.WriteAttributeString("protocolInfo", protocolInfo);
-
- writer.WriteString(info.Url);
- writer.WriteFullEndElement();
- }
-
- return true;
- }
-
- private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo)
- {
- writer.WriteStartElement(string.Empty, "res", NsDidl);
-
- var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
-
- var mediaSource = streamInfo.MediaSource;
-
- if (mediaSource?.RunTimeTicks.HasValue == true)
- {
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
- }
-
- if (filter.Contains("res@size"))
- {
- if (streamInfo.IsDirectStream || streamInfo.EstimateContentLength)
- {
- var size = streamInfo.TargetSize;
-
- if (size.HasValue)
- {
- writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
- }
- }
- }
-
- var totalBitrate = streamInfo.TargetTotalBitrate;
- var targetSampleRate = streamInfo.TargetAudioSampleRate;
- var targetChannels = streamInfo.TargetAudioChannels;
-
- var targetWidth = streamInfo.TargetWidth;
- var targetHeight = streamInfo.TargetHeight;
-
- if (targetChannels.HasValue)
- {
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- if (filter.Contains("res@resolution"))
- {
- if (targetWidth.HasValue && targetHeight.HasValue)
- {
- writer.WriteAttributeString(
- "resolution",
- string.Format(
- CultureInfo.InvariantCulture,
- "{0}x{1}",
- targetWidth.Value,
- targetHeight.Value));
- }
- }
-
- if (targetSampleRate.HasValue)
- {
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- if (totalBitrate.HasValue)
- {
- writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- var mediaProfile = _profile.GetVideoMediaProfile(
- streamInfo.Container,
- streamInfo.TargetAudioCodec.FirstOrDefault(),
- streamInfo.TargetVideoCodec.FirstOrDefault(),
- streamInfo.TargetAudioBitrate,
- targetWidth,
- targetHeight,
- streamInfo.TargetVideoBitDepth,
- streamInfo.TargetVideoProfile,
- streamInfo.TargetVideoRangeType,
- streamInfo.TargetVideoLevel,
- streamInfo.TargetFramerate ?? 0,
- streamInfo.TargetPacketLength,
- streamInfo.TargetTimestamp,
- streamInfo.IsTargetAnamorphic,
- streamInfo.IsTargetInterlaced,
- streamInfo.TargetRefFrames,
- streamInfo.TargetVideoStreamCount,
- streamInfo.TargetAudioStreamCount,
- streamInfo.TargetVideoCodecTag,
- streamInfo.IsTargetAVC);
-
- var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal));
-
- var mimeType = mediaProfile is null || string.IsNullOrEmpty(mediaProfile.MimeType)
- ? MimeTypes.GetMimeType(filename)
- : mediaProfile.MimeType;
-
- writer.WriteAttributeString(
- "protocolInfo",
- string.Format(
- CultureInfo.InvariantCulture,
- "http-get:*:{0}:{1}",
- mimeType,
- contentFeatures));
-
- writer.WriteString(url);
-
- writer.WriteFullEndElement();
- }
-
- private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context)
- {
- if (itemStubType.HasValue)
- {
- switch (itemStubType.Value)
- {
- case StubType.Latest: return _localization.GetLocalizedString("Latest");
- case StubType.Playlists: return _localization.GetLocalizedString("Playlists");
- case StubType.AlbumArtists: return _localization.GetLocalizedString("HeaderAlbumArtists");
- case StubType.Albums: return _localization.GetLocalizedString("Albums");
- case StubType.Artists: return _localization.GetLocalizedString("Artists");
- case StubType.Songs: return _localization.GetLocalizedString("Songs");
- case StubType.Genres: return _localization.GetLocalizedString("Genres");
- case StubType.FavoriteAlbums: return _localization.GetLocalizedString("HeaderFavoriteAlbums");
- case StubType.FavoriteArtists: return _localization.GetLocalizedString("HeaderFavoriteArtists");
- case StubType.FavoriteSongs: return _localization.GetLocalizedString("HeaderFavoriteSongs");
- case StubType.ContinueWatching: return _localization.GetLocalizedString("HeaderContinueWatching");
- case StubType.Movies: return _localization.GetLocalizedString("Movies");
- case StubType.Collections: return _localization.GetLocalizedString("Collections");
- case StubType.Favorites: return _localization.GetLocalizedString("Favorites");
- case StubType.NextUp: return _localization.GetLocalizedString("HeaderNextUp");
- case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
- case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
- case StubType.Series: return _localization.GetLocalizedString("Shows");
- }
- }
-
- return item is Episode episode
- ? GetEpisodeDisplayName(episode, context)
- : item.Name;
- }
-
- /// <summary>
- /// Gets episode display name appropriate for the given context.
- /// </summary>
- /// <remarks>
- /// If context is a season, this will return a string containing just episode number and name.
- /// Otherwise the result will include series names and season number.
- /// </remarks>
- /// <param name="episode">The episode.</param>
- /// <param name="context">Current context.</param>
- /// <returns>Formatted name of the episode.</returns>
- private string GetEpisodeDisplayName(Episode episode, BaseItem? context)
- {
- string[] components;
-
- if (context is Season season)
- {
- // This is a special embedded within a season
- if (episode.ParentIndexNumber.HasValue && episode.ParentIndexNumber.Value == 0
- && season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
- {
- return string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("ValueSpecialEpisodeName"),
- episode.Name);
- }
-
- // inside a season use simple format (ex. '12 - Episode Name')
- var epNumberName = GetEpisodeIndexFullName(episode);
- components = new[] { epNumberName, episode.Name };
- }
- else
- {
- // outside a season include series and season details (ex. 'TV Show - S05E11 - Episode Name')
- var epNumberName = GetEpisodeNumberDisplayName(episode);
- components = new[] { episode.SeriesName, epNumberName, episode.Name };
- }
-
- return string.Join(" - ", components.Where(NotNullOrWhiteSpace));
- }
-
- /// <summary>
- /// Gets complete episode number.
- /// </summary>
- /// <param name="episode">The episode.</param>
- /// <returns>For single episodes returns just the number. For double episodes - current and ending numbers.</returns>
- private string GetEpisodeIndexFullName(Episode episode)
- {
- var name = string.Empty;
- if (episode.IndexNumber.HasValue)
- {
- name += episode.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
-
- if (episode.IndexNumberEnd.HasValue)
- {
- name += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
- }
- }
-
- return name;
- }
-
- /// <summary>
- /// Gets episode number formatted as 'S##E##'.
- /// </summary>
- /// <param name="episode">The episode.</param>
- /// <returns>Formatted episode number.</returns>
- private string GetEpisodeNumberDisplayName(Episode episode)
- {
- var name = string.Empty;
- var seasonNumber = episode.Season?.IndexNumber;
-
- if (seasonNumber.HasValue)
- {
- name = "S" + seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture);
- }
-
- var indexName = GetEpisodeIndexFullName(episode);
-
- if (!string.IsNullOrWhiteSpace(indexName))
- {
- name += "E" + indexName;
- }
-
- return name;
- }
-
- private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
-
- private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null)
- {
- writer.WriteStartElement(string.Empty, "res", NsDidl);
-
- if (streamInfo is null)
- {
- var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user);
-
- streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
- {
- ItemId = audio.Id,
- MediaSources = sources.ToArray(),
- Profile = _profile,
- DeviceId = deviceId
- }) ?? throw new InvalidOperationException("No optimal audio stream found");
- }
-
- var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
-
- var mediaSource = streamInfo.MediaSource;
-
- if (mediaSource?.RunTimeTicks is not null)
- {
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
- }
-
- if (filter.Contains("res@size"))
- {
- if (streamInfo.IsDirectStream || streamInfo.EstimateContentLength)
- {
- var size = streamInfo.TargetSize;
-
- if (size.HasValue)
- {
- writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
- }
- }
- }
-
- var targetAudioBitrate = streamInfo.TargetAudioBitrate;
- var targetSampleRate = streamInfo.TargetAudioSampleRate;
- var targetChannels = streamInfo.TargetAudioChannels;
- var targetAudioBitDepth = streamInfo.TargetAudioBitDepth;
-
- if (targetChannels.HasValue)
- {
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- if (targetSampleRate.HasValue)
- {
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- if (targetAudioBitrate.HasValue)
- {
- writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- var mediaProfile = _profile.GetAudioMediaProfile(
- streamInfo.Container,
- streamInfo.TargetAudioCodec.FirstOrDefault(),
- targetChannels,
- targetAudioBitrate,
- targetSampleRate,
- targetAudioBitDepth);
-
- var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal));
-
- var mimeType = mediaProfile is null || string.IsNullOrEmpty(mediaProfile.MimeType)
- ? MimeTypes.GetMimeType(filename)
- : mediaProfile.MimeType;
-
- var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
- _profile,
- streamInfo.Container,
- streamInfo.TargetAudioCodec.FirstOrDefault(),
- targetAudioBitrate,
- targetSampleRate,
- targetChannels,
- targetAudioBitDepth,
- streamInfo.IsDirectStream,
- streamInfo.RunTimeTicks ?? 0,
- streamInfo.TranscodeSeekInfo);
-
- writer.WriteAttributeString(
- "protocolInfo",
- string.Format(
- CultureInfo.InvariantCulture,
- "http-get:*:{0}:{1}",
- mimeType,
- contentFeatures));
-
- writer.WriteString(url);
-
- writer.WriteFullEndElement();
- }
-
- public static bool IsIdRoot(string id)
- => string.IsNullOrWhiteSpace(id)
- || string.Equals(id, "0", StringComparison.OrdinalIgnoreCase)
- // Samsung sometimes uses 1 as root
- || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
-
- public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null)
- {
- writer.WriteStartElement(string.Empty, "container", NsDidl);
-
- writer.WriteAttributeString("restricted", "1");
- writer.WriteAttributeString("searchable", "1");
- writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
-
- var clientId = GetClientId(folder, stubType);
-
- if (string.Equals(requestedId, "0", StringComparison.Ordinal))
- {
- writer.WriteAttributeString("id", "0");
- writer.WriteAttributeString("parentID", "-1");
- }
- else
- {
- writer.WriteAttributeString("id", clientId);
-
- if (context is not null)
- {
- writer.WriteAttributeString("parentID", GetClientId(context, null));
- }
- else
- {
- var parent = folder.DisplayParentId;
- if (parent.Equals(default))
- {
- writer.WriteAttributeString("parentID", "0");
- }
- else
- {
- writer.WriteAttributeString("parentID", GetClientId(parent, null));
- }
- }
- }
-
- AddGeneralProperties(folder, stubType, context, writer, filter);
-
- AddCover(folder, stubType, writer);
-
- writer.WriteFullEndElement();
- }
-
- private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo)
- {
- if (!item.SupportsPositionTicksResume || item is Folder)
- {
- return;
- }
-
- XmlAttribute? secAttribute = null;
- foreach (var attribute in _profile.XmlRootAttributes)
- {
- if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
- {
- secAttribute = attribute;
- break;
- }
- }
-
- // Not a samsung device or no user data
- if (secAttribute is null || user is null)
- {
- return;
- }
-
- var userdata = _userDataManager.GetUserData(user, item);
- var playbackPositionTicks = (streamInfo is not null && streamInfo.StartPositionTicks > 0) ? streamInfo.StartPositionTicks : userdata.PlaybackPositionTicks;
-
- if (playbackPositionTicks > 0)
- {
- var elementValue = string.Format(
- CultureInfo.InvariantCulture,
- "BM={0}",
- Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds));
- AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value);
- }
- }
-
- /// <summary>
- /// Adds fields used by both items and folders.
- /// </summary>
- private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
- {
- // Don't filter on dc:title because not all devices will include it in the filter
- // MediaMonkey for example won't display content without a title
- // if (filter.Contains("dc:title"))
- {
- AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc);
- }
-
- WriteObjectClass(writer, item, itemStubType);
-
- if (filter.Contains("dc:date"))
- {
- if (item.PremiereDate.HasValue)
- {
- AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
- }
- }
-
- if (filter.Contains("upnp:genre"))
- {
- foreach (var genre in item.Genres)
- {
- AddValue(writer, "upnp", "genre", genre, NsUpnp);
- }
- }
-
- foreach (var studio in item.Studios)
- {
- AddValue(writer, "upnp", "publisher", studio, NsUpnp);
- }
-
- if (item is not Folder)
- {
- if (filter.Contains("dc:description"))
- {
- var desc = item.Overview;
-
- if (!string.IsNullOrWhiteSpace(desc))
- {
- AddValue(writer, "dc", "description", desc, NsDc);
- }
- }
-
- // if (filter.Contains("upnp:longDescription"))
- // {
- // if (!string.IsNullOrWhiteSpace(item.Overview))
- // {
- // AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp);
- // }
- // }
- }
-
- if (!string.IsNullOrEmpty(item.OfficialRating))
- {
- if (filter.Contains("dc:rating"))
- {
- AddValue(writer, "dc", "rating", item.OfficialRating, NsDc);
- }
-
- if (filter.Contains("upnp:rating"))
- {
- AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp);
- }
- }
-
- AddPeople(item, writer);
- }
-
- private void WriteObjectClass(XmlWriter writer, BaseItem item, StubType? stubType)
- {
- // More types here
- // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs
-
- writer.WriteStartElement("upnp", "class", NsUpnp);
-
- if (item.IsDisplayedAsFolder || stubType.HasValue)
- {
- string? classType = null;
-
- if (!_profile.RequiresPlainFolders)
- {
- if (item is MusicAlbum)
- {
- classType = "object.container.album.musicAlbum";
- }
- else if (item is MusicArtist)
- {
- classType = "object.container.person.musicArtist";
- }
- else if (item is Series || item is Season || item is BoxSet || item is Video)
- {
- classType = "object.container.album.videoAlbum";
- }
- else if (item is Playlist)
- {
- classType = "object.container.playlistContainer";
- }
- else if (item is PhotoAlbum)
- {
- classType = "object.container.album.photoAlbum";
- }
- }
-
- writer.WriteString(classType ?? "object.container.storageFolder");
- }
- else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
- {
- writer.WriteString("object.item.audioItem.musicTrack");
- }
- else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
- {
- writer.WriteString("object.item.imageItem.photo");
- }
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- if (!_profile.RequiresPlainVideoItems && item is Movie)
- {
- writer.WriteString("object.item.videoItem.movie");
- }
- else if (!_profile.RequiresPlainVideoItems && item is MusicVideo)
- {
- writer.WriteString("object.item.videoItem.musicVideoClip");
- }
- else
- {
- writer.WriteString("object.item.videoItem");
- }
- }
- else if (item is MusicGenre)
- {
- writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre.musicGenre");
- }
- else if (item is Genre)
- {
- writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre");
- }
- else
- {
- writer.WriteString("object.item");
- }
-
- writer.WriteFullEndElement();
- }
-
- private void AddPeople(BaseItem item, XmlWriter writer)
- {
- if (!item.SupportsPeople)
- {
- return;
- }
-
- var types = new[]
- {
- PersonKind.Director,
- PersonKind.Writer,
- PersonKind.Producer,
- PersonKind.Composer,
- PersonKind.Creator
- };
-
- // Seeing some LG models locking up due content with large lists of people
- // The actual issue might just be due to processing a more metadata than it can handle
- var people = _libraryManager.GetPeople(
- new InternalPeopleQuery
- {
- ItemId = item.Id,
- Limit = 6
- });
-
- foreach (var actor in people)
- {
- var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
- if (type == PersonKind.Unknown)
- {
- type = PersonKind.Actor;
- }
-
- AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
- }
- }
-
- private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
- {
- AddCommonFields(item, itemStubType, context, writer, filter);
-
- var hasAlbumArtists = item as IHasAlbumArtist;
-
- if (item is IHasArtist hasArtists)
- {
- foreach (var artist in hasArtists.Artists)
- {
- AddValue(writer, "upnp", "artist", artist, NsUpnp);
- AddValue(writer, "dc", "creator", artist, NsDc);
-
- // If it doesn't support album artists (musicvideo), then tag as both
- if (hasAlbumArtists is null)
- {
- AddAlbumArtist(writer, artist);
- }
- }
- }
-
- if (hasAlbumArtists is not null)
- {
- foreach (var albumArtist in hasAlbumArtists.AlbumArtists)
- {
- AddAlbumArtist(writer, albumArtist);
- }
- }
-
- if (!string.IsNullOrWhiteSpace(item.Album))
- {
- AddValue(writer, "upnp", "album", item.Album, NsUpnp);
- }
-
- if (item.IndexNumber.HasValue)
- {
- AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
-
- if (item is Episode)
- {
- AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
- }
- }
- }
-
- private void AddAlbumArtist(XmlWriter writer, string name)
- {
- try
- {
- writer.WriteStartElement("upnp", "artist", NsUpnp);
- writer.WriteAttributeString("role", "AlbumArtist");
-
- writer.WriteString(name);
-
- writer.WriteFullEndElement();
- }
- catch (XmlException ex)
- {
- _logger.LogError(ex, "Error adding xml value: {Value}", name);
- }
- }
-
- private void AddValue(XmlWriter writer, string prefix, string name, string value, string namespaceUri)
- {
- try
- {
- writer.WriteElementString(prefix, name, namespaceUri, value);
- }
- catch (XmlException ex)
- {
- _logger.LogError(ex, "Error adding xml value: {Value}", value);
- }
- }
-
- private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
- {
- ImageDownloadInfo? imageInfo = GetImageInfo(item);
-
- if (imageInfo is null)
- {
- return;
- }
-
- // TODO: Remove these default values
- var albumArtUrlInfo = GetImageUrl(
- imageInfo,
- _profile.MaxAlbumArtWidth ?? 10000,
- _profile.MaxAlbumArtHeight ?? 10000,
- "jpg");
-
- writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
- if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
- {
- writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
- }
-
- writer.WriteString(albumArtUrlInfo.Url);
- writer.WriteFullEndElement();
-
- // TODO: Remove these default values
- var iconUrlInfo = GetImageUrl(
- imageInfo,
- _profile.MaxIconWidth ?? 48,
- _profile.MaxIconHeight ?? 48,
- "jpg");
- writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.Url);
-
- if (!_profile.EnableAlbumArtInDidl)
- {
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- || string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- if (!stubType.HasValue)
- {
- return;
- }
- }
- }
-
- if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
- {
- AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
- AddImageResElement(item, writer, 1024, 768, "jpg", "JPEG_MED");
- AddImageResElement(item, writer, 640, 480, "jpg", "JPEG_SM");
- AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG");
- AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
- }
-
- AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
- }
-
- private void AddImageResElement(
- BaseItem item,
- XmlWriter writer,
- int maxWidth,
- int maxHeight,
- string format,
- string org_Pn)
- {
- var imageInfo = GetImageInfo(item);
-
- if (imageInfo is null)
- {
- return;
- }
-
- var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
-
- writer.WriteStartElement(string.Empty, "res", NsDidl);
-
- // Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
- // rather than using a larger one when available
- var width = albumartUrlInfo.Width ?? maxWidth;
- var height = albumartUrlInfo.Height ?? maxHeight;
-
- var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
-
- writer.WriteAttributeString(
- "protocolInfo",
- string.Format(
- CultureInfo.InvariantCulture,
- "http-get:*:{0}:{1}",
- MimeTypes.GetMimeType("file." + format),
- contentFeatures));
-
- writer.WriteAttributeString(
- "resolution",
- string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
-
- writer.WriteString(albumartUrlInfo.Url);
-
- writer.WriteFullEndElement();
- }
-
- private ImageDownloadInfo? GetImageInfo(BaseItem item)
- {
- if (item.HasImage(ImageType.Primary))
- {
- return GetImageInfo(item, ImageType.Primary);
- }
-
- if (item.HasImage(ImageType.Thumb))
- {
- return GetImageInfo(item, ImageType.Thumb);
- }
-
- if (item.HasImage(ImageType.Backdrop))
- {
- if (item is Channel)
- {
- return GetImageInfo(item, ImageType.Backdrop);
- }
- }
-
- // For audio tracks without art use album art if available.
- if (item is Audio audioItem)
- {
- var album = audioItem.AlbumEntity;
- return album is not null && album.HasImage(ImageType.Primary)
- ? GetImageInfo(album, ImageType.Primary)
- : null;
- }
-
- // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder.
- if (item is MusicAlbum || item is Playlist)
- {
- return null;
- }
-
- // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item.
- var parentWithImage = GetFirstParentWithImageBelowUserRoot(item);
- if (parentWithImage is not null)
- {
- return GetImageInfo(parentWithImage, ImageType.Primary);
- }
-
- return null;
- }
-
- private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
- {
- if (item is null)
- {
- return null;
- }
-
- if (item.HasImage(ImageType.Primary))
- {
- return item;
- }
-
- var parent = item.GetParent();
- if (parent is UserRootFolder)
- {
- return null;
- }
-
- // terminate in case we went past user root folder (unlikely?)
- if (parent is Folder folder && folder.IsRoot)
- {
- return null;
- }
-
- return GetFirstParentWithImageBelowUserRoot(parent);
- }
-
- private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
- {
- var imageInfo = item.GetImageInfo(type, 0);
- string? tag = null;
-
- try
- {
- tag = _imageProcessor.GetImageCacheTag(item, type);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting image cache tag");
- }
-
- int? width = imageInfo.Width;
- int? height = imageInfo.Height;
-
- if (width == 0 || height == 0)
- {
- width = null;
- height = null;
- }
- else if (width == -1 || height == -1)
- {
- width = null;
- height = null;
- }
-
- var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty)
- .TrimStart('.')
- .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
-
- return new ImageDownloadInfo
- {
- ItemId = item.Id,
- Type = type,
- ImageTag = tag,
- Width = width,
- Height = height,
- Format = inputFormat,
- ItemImageInfo = imageInfo
- };
- }
-
- public static string GetClientId(BaseItem item, StubType? stubType)
- {
- return GetClientId(item.Id, stubType);
- }
-
- public static string GetClientId(Guid idValue, StubType? stubType)
- {
- var id = idValue.ToString("N", CultureInfo.InvariantCulture);
-
- if (stubType.HasValue)
- {
- id = stubType.Value.ToString().ToLowerInvariant() + "_" + id;
- }
-
- return id;
- }
-
- private (string Url, int? Width, int? Height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
- {
- var url = string.Format(
- CultureInfo.InvariantCulture,
- "{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0",
- _serverAddress,
- info.ItemId.ToString("N", CultureInfo.InvariantCulture),
- info.Type,
- info.ImageTag,
- format,
- maxWidth.ToString(CultureInfo.InvariantCulture),
- maxHeight.ToString(CultureInfo.InvariantCulture));
-
- var width = info.Width;
- var height = info.Height;
-
- info.IsDirectStream = false;
-
- if (width.HasValue && height.HasValue)
- {
- var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
-
- width = newSize.Width;
- height = newSize.Height;
-
- var normalizedFormat = format
- .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
-
- if (string.Equals(info.Format, normalizedFormat, StringComparison.OrdinalIgnoreCase))
- {
- info.IsDirectStream = maxWidth >= width.Value && maxHeight >= height.Value;
- }
- }
-
- // just lie
- info.IsDirectStream = true;
-
- return (url, width, height);
- }
-
- private class ImageDownloadInfo
- {
- internal Guid ItemId { get; set; }
-
- internal string? ImageTag { get; set; }
-
- internal ImageType Type { get; set; }
-
- internal int? Width { get; set; }
-
- internal int? Height { get; set; }
-
- internal bool IsDirectStream { get; set; }
-
- internal required string Format { get; set; }
-
- internal required ItemImageInfo ItemImageInfo { get; set; }
- }
- }
-}
diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs
deleted file mode 100644
index 6db6f3ae3..000000000
--- a/Emby.Dlna/Didl/Filter.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Dlna.Didl
-{
- public class Filter
- {
- private readonly string[] _fields;
- private readonly bool _all;
-
- public Filter()
- : this("*")
- {
- }
-
- public Filter(string filter)
- {
- _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
- _fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
- }
-
- public bool Contains(string field)
- {
- return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
- }
- }
-}
diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs
deleted file mode 100644
index b66f53ece..000000000
--- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable CA1305
-
-using System;
-using System.IO;
-using System.Text;
-
-namespace Emby.Dlna.Didl
-{
- public class StringWriterWithEncoding : StringWriter
- {
- private readonly Encoding? _encoding;
-
- public StringWriterWithEncoding()
- {
- }
-
- public StringWriterWithEncoding(IFormatProvider formatProvider)
- : base(formatProvider)
- {
- }
-
- public StringWriterWithEncoding(StringBuilder sb)
- : base(sb)
- {
- }
-
- public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider)
- : base(sb, formatProvider)
- {
- }
-
- public StringWriterWithEncoding(Encoding encoding)
- {
- _encoding = encoding;
- }
-
- public StringWriterWithEncoding(IFormatProvider formatProvider, Encoding encoding)
- : base(formatProvider)
- {
- _encoding = encoding;
- }
-
- public StringWriterWithEncoding(StringBuilder sb, Encoding encoding)
- : base(sb)
- {
- _encoding = encoding;
- }
-
- public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider, Encoding encoding)
- : base(sb, formatProvider)
- {
- _encoding = encoding;
- }
-
- public override Encoding Encoding => _encoding ?? base.Encoding;
- }
-}
diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs
deleted file mode 100644
index 6cc6b73a0..000000000
--- a/Emby.Dlna/DlnaConfigurationFactory.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using Emby.Dlna.Configuration;
-using MediaBrowser.Common.Configuration;
-
-namespace Emby.Dlna
-{
- public class DlnaConfigurationFactory : IConfigurationFactory
- {
- public IEnumerable<ConfigurationStore> GetConfigurations()
- {
- return new[]
- {
- new ConfigurationStore
- {
- Key = "dlna",
- ConfigurationType = typeof(DlnaOptions)
- }
- };
- }
- }
-}
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
deleted file mode 100644
index d67cb67b5..000000000
--- a/Emby.Dlna/DlnaManager.cs
+++ /dev/null
@@ -1,491 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Text.Json;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-using Emby.Dlna.Profiles;
-using Emby.Dlna.Server;
-using Jellyfin.Extensions.Json;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-
-namespace Emby.Dlna
-{
- public class DlnaManager : IDlnaManager
- {
- private readonly IApplicationPaths _appPaths;
- private readonly IXmlSerializer _xmlSerializer;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<DlnaManager> _logger;
- private readonly IServerApplicationHost _appHost;
- private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
-
- private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
-
- public DlnaManager(
- IXmlSerializer xmlSerializer,
- IFileSystem fileSystem,
- IApplicationPaths appPaths,
- ILoggerFactory loggerFactory,
- IServerApplicationHost appHost)
- {
- _xmlSerializer = xmlSerializer;
- _fileSystem = fileSystem;
- _appPaths = appPaths;
- _logger = loggerFactory.CreateLogger<DlnaManager>();
- _appHost = appHost;
- }
-
- private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
-
- private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
-
- public async Task InitProfilesAsync()
- {
- try
- {
- await ExtractSystemProfilesAsync().ConfigureAwait(false);
- Directory.CreateDirectory(UserProfilesPath);
- LoadProfiles();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error extracting DLNA profiles.");
- }
- }
-
- private void LoadProfiles()
- {
- var list = GetProfiles(UserProfilesPath, DeviceProfileType.User)
- .OrderBy(i => i.Name)
- .ToList();
-
- list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System)
- .OrderBy(i => i.Name));
- }
-
- public IEnumerable<DeviceProfile> GetProfiles()
- {
- lock (_profiles)
- {
- return _profiles.Values
- .OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
- .ThenBy(i => i.Item1.Info.Name)
- .Select(i => i.Item2)
- .ToList();
- }
- }
-
- /// <inheritdoc />
- public DeviceProfile GetDefaultProfile()
- {
- return new DefaultProfile();
- }
-
- /// <inheritdoc />
- public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
- {
- ArgumentNullException.ThrowIfNull(deviceInfo);
-
- var profile = GetProfiles()
- .FirstOrDefault(i => i.Identification is not null && IsMatch(deviceInfo, i.Identification));
-
- if (profile is null)
- {
- _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
- }
- else
- {
- _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
- }
-
- return profile;
- }
-
- /// <summary>
- /// Attempts to match a device with a profile.
- /// Rules:
- /// - If the profile field has no value, the field matches regardless of its contents.
- /// - the profile field can be an exact match, or a reg exp.
- /// </summary>
- /// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
- /// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
- /// <returns><b>True</b> if they match.</returns>
- public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
- {
- return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
- && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
- && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
- && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
- && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
- && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
- && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
- && IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
- }
-
- private bool IsRegexOrSubstringMatch(string input, string pattern)
- {
- if (string.IsNullOrEmpty(pattern))
- {
- // In profile identification: An empty pattern matches anything.
- return true;
- }
-
- if (string.IsNullOrEmpty(input))
- {
- // The profile contains a value, and the device doesn't.
- return false;
- }
-
- try
- {
- return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
- || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
- }
- catch (ArgumentException ex)
- {
- _logger.LogError(ex, "Error evaluating regex pattern {Pattern}", pattern);
- return false;
- }
- }
-
- /// <inheritdoc />
- public DeviceProfile? GetProfile(IHeaderDictionary headers)
- {
- ArgumentNullException.ThrowIfNull(headers);
-
- var profile = GetProfiles().FirstOrDefault(i => i.Identification is not null && IsMatch(headers, i.Identification));
- if (profile is null)
- {
- _logger.LogDebug("No matching device profile found. {@Headers}", headers);
- }
- else
- {
- _logger.LogDebug("Found matching device profile: {0}", profile.Name);
- }
-
- return profile;
- }
-
- private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo)
- {
- return profileInfo.Headers.Any(i => IsMatch(headers, i));
- }
-
- private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header)
- {
- // Handle invalid user setup
- if (string.IsNullOrEmpty(header.Name))
- {
- return false;
- }
-
- if (headers.TryGetValue(header.Name, out StringValues value))
- {
- if (StringValues.IsNullOrEmpty(value))
- {
- return false;
- }
-
- switch (header.Match)
- {
- case HeaderMatchType.Equals:
- return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
- case HeaderMatchType.Substring:
- var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
- // _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
- return isMatch;
- case HeaderMatchType.Regex:
- // Can't be null, we checked above the switch statement
- return Regex.IsMatch(value!, header.Value, RegexOptions.IgnoreCase);
- default:
- throw new ArgumentException("Unrecognized HeaderMatchType");
- }
- }
-
- return false;
- }
-
- private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
- {
- try
- {
- return _fileSystem.GetFilePaths(path)
- .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
- .Select(i => ParseProfileFile(i, type))
- .Where(i => i is not null)
- .ToList()!; // We just filtered out all the nulls
- }
- catch (IOException)
- {
- return Array.Empty<DeviceProfile>();
- }
- }
-
- private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
- {
- lock (_profiles)
- {
- if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
- {
- return profileTuple.Item2;
- }
-
- try
- {
- var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
- var profile = ReserializeProfile(tempProfile);
-
- profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
- _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
-
- return profile;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error parsing profile file: {Path}", path);
-
- return null;
- }
- }
- }
-
- /// <inheritdoc />
- public DeviceProfile? GetProfile(string id)
- {
- ArgumentException.ThrowIfNullOrEmpty(id);
-
- var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
-
- if (info is null)
- {
- return null;
- }
-
- return ParseProfileFile(info.Path, info.Info.Type);
- }
-
- private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
- {
- lock (_profiles)
- {
- return _profiles.Values
- .Select(i => i.Item1)
- .OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
- .ThenBy(i => i.Info.Name);
- }
- }
-
- /// <inheritdoc />
- public IEnumerable<DeviceProfileInfo> GetProfileInfos()
- {
- return GetProfileInfosInternal().Select(i => i.Info);
- }
-
- private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
- {
- return new InternalProfileInfo(
- new DeviceProfileInfo
- {
- Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
- Name = _fileSystem.GetFileNameWithoutExtension(file),
- Type = type
- },
- file.FullName);
- }
-
- private async Task ExtractSystemProfilesAsync()
- {
- var namespaceName = GetType().Namespace + ".Profiles.Xml.";
-
- var systemProfilesPath = SystemProfilesPath;
-
- foreach (var name in _assembly.GetManifestResourceNames())
- {
- if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
- {
- continue;
- }
-
- var path = Path.Join(
- systemProfilesPath,
- Path.GetFileName(name.AsSpan())[namespaceName.Length..]);
-
- if (File.Exists(path))
- {
- continue;
- }
-
- // The stream should exist as we just got its name from GetManifestResourceNames
- using (var stream = _assembly.GetManifestResourceStream(name)!)
- {
- Directory.CreateDirectory(systemProfilesPath);
-
- var fileOptions = AsyncFile.WriteOptions;
- fileOptions.Mode = FileMode.CreateNew;
- fileOptions.PreallocationSize = stream.Length;
- var fileStream = new FileStream(path, fileOptions);
- await using (fileStream.ConfigureAwait(false))
- {
- await stream.CopyToAsync(fileStream).ConfigureAwait(false);
- }
- }
- }
- }
-
- /// <inheritdoc />
- public void DeleteProfile(string id)
- {
- var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
-
- if (info.Info.Type == DeviceProfileType.System)
- {
- throw new ArgumentException("System profiles cannot be deleted.");
- }
-
- _fileSystem.DeleteFile(info.Path);
-
- lock (_profiles)
- {
- _profiles.Remove(info.Path);
- }
- }
-
- /// <inheritdoc />
- public void CreateProfile(DeviceProfile profile)
- {
- profile = ReserializeProfile(profile);
-
- ArgumentException.ThrowIfNullOrEmpty(profile.Name);
-
- var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
- var path = Path.Combine(UserProfilesPath, newFilename);
-
- SaveProfile(profile, path, DeviceProfileType.User);
- }
-
- /// <inheritdoc />
- public void UpdateProfile(string profileId, DeviceProfile profile)
- {
- profile = ReserializeProfile(profile);
-
- ArgumentException.ThrowIfNullOrEmpty(profile.Id);
-
- ArgumentException.ThrowIfNullOrEmpty(profile.Name);
-
- var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
- if (current.Info.Type == DeviceProfileType.System)
- {
- throw new ArgumentException("System profiles can't be edited");
- }
-
- var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
- var path = Path.Join(UserProfilesPath, newFilename);
-
- if (!string.Equals(path, current.Path, StringComparison.Ordinal))
- {
- lock (_profiles)
- {
- _profiles.Remove(current.Path);
- }
- }
-
- SaveProfile(profile, path, DeviceProfileType.User);
- }
-
- private void SaveProfile(DeviceProfile profile, string path, DeviceProfileType type)
- {
- lock (_profiles)
- {
- _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
- }
-
- SerializeToXml(profile, path);
- }
-
- internal void SerializeToXml(DeviceProfile profile, string path)
- {
- _xmlSerializer.SerializeToFile(profile, path);
- }
-
- /// <summary>
- /// Recreates the object using serialization, to ensure it's not a subclass.
- /// 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 re-serialized device profile.</returns>
- private DeviceProfile ReserializeProfile(DeviceProfile profile)
- {
- if (profile.GetType() == typeof(DeviceProfile))
- {
- return profile;
- }
-
- var json = JsonSerializer.Serialize(profile, _jsonOptions);
-
- // Output can't be null if the input isn't null
- return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
- }
-
- /// <inheritdoc />
- public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
- {
- var profile = GetProfile(headers) ?? GetDefaultProfile();
-
- var serverId = _appHost.SystemId;
-
- return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
- }
-
- /// <inheritdoc />
- public ImageStream? GetIcon(string filename)
- {
- var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
- ? ImageFormat.Png
- : ImageFormat.Jpg;
-
- var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
- var stream = _assembly.GetManifestResourceStream(resource);
- if (stream is null)
- {
- return null;
- }
-
- return new ImageStream(stream)
- {
- Format = format
- };
- }
-
- private class InternalProfileInfo
- {
- internal InternalProfileInfo(DeviceProfileInfo info, string path)
- {
- Info = info;
- Path = path;
- }
-
- internal DeviceProfileInfo Info { get; }
-
- internal string Path { get; }
- }
- }
-}
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
deleted file mode 100644
index aca239964..000000000
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ /dev/null
@@ -1,86 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
- <PropertyGroup>
- <ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid>
- </PropertyGroup>
-
- <ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
- </ItemGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
- <ProjectReference Include="..\RSSDP\RSSDP.csproj" />
- </ItemGroup>
-
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <GenerateDocumentationFile>true</GenerateDocumentationFile>
- </PropertyGroup>
-
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
- </PropertyGroup>
-
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
- <ItemGroup>
- <EmbeddedResource Include="Images\logo120.jpg" />
- <EmbeddedResource Include="Images\logo120.png" />
- <EmbeddedResource Include="Images\logo240.jpg" />
- <EmbeddedResource Include="Images\logo240.png" />
- <EmbeddedResource Include="Images\logo48.jpg" />
- <EmbeddedResource Include="Images\logo48.png" />
- <EmbeddedResource Include="Images\people48.jpg" />
- <EmbeddedResource Include="Images\people48.png" />
- <EmbeddedResource Include="Images\people480.jpg" />
- <EmbeddedResource Include="Images\people480.png" />
- </ItemGroup>
-
- <ItemGroup>
- <EmbeddedResource Include="Profiles\Xml\Default.xml" />
- <EmbeddedResource Include="Profiles\Xml\Denon AVR.xml" />
- <EmbeddedResource Include="Profiles\Xml\DirecTV HD-DVR.xml" />
- <EmbeddedResource Include="Profiles\Xml\Dish Hopper-Joey.xml" />
- <EmbeddedResource Include="Profiles\Xml\foobar2000.xml" />
- <EmbeddedResource Include="Profiles\Xml\LG Smart TV.xml" />
- <EmbeddedResource Include="Profiles\Xml\Linksys DMA2100.xml" />
- <EmbeddedResource Include="Profiles\Xml\Marantz.xml" />
- <EmbeddedResource Include="Profiles\Xml\MediaMonkey.xml" />
- <EmbeddedResource Include="Profiles\Xml\Panasonic Viera.xml" />
- <EmbeddedResource Include="Profiles\Xml\Popcorn Hour.xml" />
- <EmbeddedResource Include="Profiles\Xml\Samsung Smart TV.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2013.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2014.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2015.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2016.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282010%29.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282011%29.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282012%29.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282013%29.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282014%29.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 3.xml" />
- <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 4.xml" />
- <EmbeddedResource Include="Profiles\Xml\WDTV Live.xml" />
- <EmbeddedResource Include="Profiles\Xml\Xbox One.xml" />
- </ItemGroup>
-
- <ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Http" />
- </ItemGroup>
-
-</Project>
diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs
deleted file mode 100644
index 635d2c47a..000000000
--- a/Emby.Dlna/EventSubscriptionResponse.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace Emby.Dlna
-{
- public class EventSubscriptionResponse
- {
- public EventSubscriptionResponse(string content, string contentType)
- {
- Content = content;
- ContentType = contentType;
- Headers = new Dictionary<string, string>();
- }
-
- public string Content { get; set; }
-
- public string ContentType { get; set; }
-
- public Dictionary<string, string> Headers { get; }
- }
-}
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
deleted file mode 100644
index ecbbdf9df..000000000
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ /dev/null
@@ -1,183 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Net.Mime;
-using System.Text;
-using System.Threading.Tasks;
-using Jellyfin.Extensions;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.Eventing
-{
- public class DlnaEventManager : IDlnaEventManager
- {
- private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
- new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
-
- private readonly ILogger _logger;
- private readonly IHttpClientFactory _httpClientFactory;
-
- public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
- {
- _httpClientFactory = httpClientFactory;
- _logger = logger;
- }
-
- public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
- {
- var subscription = GetSubscription(subscriptionId, false);
- if (subscription is not null)
- {
- subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
- int timeoutSeconds = subscription.TimeoutSeconds;
- subscription.SubscriptionTime = DateTime.UtcNow;
-
- _logger.LogDebug(
- "Renewing event subscription for {0} with timeout of {1} to {2}",
- subscription.NotificationType,
- timeoutSeconds,
- subscription.CallbackUrl);
-
- return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
- }
-
- return new EventSubscriptionResponse(string.Empty, "text/plain");
- }
-
- public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
- {
- var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
- var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-
- _logger.LogDebug(
- "Creating event subscription for {0} with timeout of {1} to {2}",
- notificationType,
- timeout,
- callbackUrl);
-
- _subscriptions.TryAdd(id, new EventSubscription
- {
- Id = id,
- CallbackUrl = callbackUrl,
- SubscriptionTime = DateTime.UtcNow,
- TimeoutSeconds = timeout,
- NotificationType = notificationType
- });
-
- return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);
- }
-
- private int? ParseTimeout(string header)
- {
- if (!string.IsNullOrEmpty(header))
- {
- // Starts with SECOND-
- if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
- {
- return val;
- }
- }
-
- return null;
- }
-
- public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
- {
- _logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
-
- _subscriptions.TryRemove(subscriptionId, out _);
-
- return new EventSubscriptionResponse(string.Empty, "text/plain");
- }
-
- private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
- {
- var response = new EventSubscriptionResponse(string.Empty, "text/plain");
-
- response.Headers["SID"] = subscriptionId;
- response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
-
- return response;
- }
-
- public EventSubscription GetSubscription(string id)
- {
- return GetSubscription(id, false);
- }
-
- private EventSubscription GetSubscription(string id, bool throwOnMissing)
- {
- if (!_subscriptions.TryGetValue(id, out EventSubscription e) && throwOnMissing)
- {
- throw new ResourceNotFoundException("Event with Id " + id + " not found.");
- }
-
- return e;
- }
-
- public Task TriggerEvent(string notificationType, IDictionary<string, string> stateVariables)
- {
- var subs = _subscriptions.Values
- .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase));
-
- var tasks = subs.Select(i => TriggerEvent(i, stateVariables));
-
- return Task.WhenAll(tasks);
- }
-
- private async Task TriggerEvent(EventSubscription subscription, IDictionary<string, string> stateVariables)
- {
- var builder = new StringBuilder();
-
- builder.Append("<?xml version=\"1.0\"?>");
- builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
- foreach (var key in stateVariables.Keys)
- {
- builder.Append("<e:property>")
- .Append('<')
- .Append(key)
- .Append('>')
- .Append(stateVariables[key])
- .Append("</")
- .Append(key)
- .Append('>')
- .Append("</e:property>");
- }
-
- builder.Append("</e:propertyset>");
-
- using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
- options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
- options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
- options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
- options.Headers.TryAddWithoutValidation("SID", subscription.Id);
- options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
-
- try
- {
- using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
- .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- }
- catch
- {
- // Already logged at lower levels
- }
- finally
- {
- subscription.IncrementTriggerCount();
- }
- }
- }
-}
diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs
deleted file mode 100644
index 4fd7f8169..000000000
--- a/Emby.Dlna/Eventing/EventSubscription.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Dlna.Eventing
-{
- public class EventSubscription
- {
- public string Id { get; set; }
-
- public string CallbackUrl { get; set; }
-
- public string NotificationType { get; set; }
-
- public DateTime SubscriptionTime { get; set; }
-
- public int TimeoutSeconds { get; set; }
-
- public long TriggerCount { get; set; }
-
- public bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow;
-
- public void IncrementTriggerCount()
- {
- if (TriggerCount == long.MaxValue)
- {
- TriggerCount = 0;
- }
-
- TriggerCount++;
- }
- }
-}
diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
deleted file mode 100644
index 87ec14d95..000000000
--- a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System;
-using System.Globalization;
-using System.Net;
-using System.Net.Http;
-using System.Text;
-using Emby.Dlna.ConnectionManager;
-using Emby.Dlna.ContentDirectory;
-using Emby.Dlna.MediaReceiverRegistrar;
-using Emby.Dlna.Ssdp;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Rssdp.Infrastructure;
-
-namespace Emby.Dlna.Extensions;
-
-/// <summary>
-/// Extension methods for adding DLNA services.
-/// </summary>
-public static class DlnaServiceCollectionExtensions
-{
- /// <summary>
- /// Adds DLNA services to the provided <see cref="IServiceCollection"/>.
- /// </summary>
- /// <param name="services">The <see cref="IServiceCollection"/>.</param>
- /// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param>
- public static void AddDlnaServices(
- this IServiceCollection services,
- IServerApplicationHost applicationHost)
- {
- services.AddHttpClient(NamedClient.Dlna, c =>
- {
- c.DefaultRequestHeaders.UserAgent.ParseAdd(
- string.Format(
- CultureInfo.InvariantCulture,
- "{0}/{1} UPnP/1.0 {2}/{3}",
- Environment.OSVersion.Platform,
- Environment.OSVersion,
- applicationHost.Name,
- applicationHost.ApplicationVersionString));
-
- c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
- c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
- })
- .ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
- {
- AutomaticDecompression = DecompressionMethods.All,
- RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
- });
-
- services.AddSingleton<IDlnaManager, DlnaManager>();
- services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
- services.AddSingleton<IContentDirectory, ContentDirectoryService>();
- services.AddSingleton<IConnectionManager, ConnectionManagerService>();
- services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>();
-
- services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer(
- provider.GetRequiredService<ISocketFactory>(),
- provider.GetRequiredService<INetworkManager>(),
- provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>())
- {
- IsShared = true
- });
- }
-}
diff --git a/Emby.Dlna/IConnectionManager.cs b/Emby.Dlna/IConnectionManager.cs
deleted file mode 100644
index 9f643a9e6..000000000
--- a/Emby.Dlna/IConnectionManager.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Dlna
-{
- public interface IConnectionManager : IDlnaEventManager, IUpnpService
- {
- }
-}
diff --git a/Emby.Dlna/IContentDirectory.cs b/Emby.Dlna/IContentDirectory.cs
deleted file mode 100644
index 10f4d6386..000000000
--- a/Emby.Dlna/IContentDirectory.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Dlna
-{
- public interface IContentDirectory : IDlnaEventManager, IUpnpService
- {
- }
-}
diff --git a/Emby.Dlna/IDlnaEventManager.cs b/Emby.Dlna/IDlnaEventManager.cs
deleted file mode 100644
index bb1eeb963..000000000
--- a/Emby.Dlna/IDlnaEventManager.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace Emby.Dlna
-{
- public interface IDlnaEventManager
- {
- /// <summary>
- /// Cancels the event subscription.
- /// </summary>
- /// <param name="subscriptionId">The subscription identifier.</param>
- /// <returns>The response.</returns>
- EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
-
- /// <summary>
- /// Renews the event subscription.
- /// </summary>
- /// <param name="subscriptionId">The subscription identifier.</param>
- /// <param name="notificationType">The notification type.</param>
- /// <param name="requestedTimeoutString">The requested timeout as a string.</param>
- /// <param name="callbackUrl">The callback url.</param>
- /// <returns>The response.</returns>
- EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
-
- /// <summary>
- /// Creates the event subscription.
- /// </summary>
- /// <param name="notificationType">The notification type.</param>
- /// <param name="requestedTimeoutString">The requested timeout as a string.</param>
- /// <param name="callbackUrl">The callback url.</param>
- /// <returns>The response.</returns>
- EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
- }
-}
diff --git a/Emby.Dlna/IMediaReceiverRegistrar.cs b/Emby.Dlna/IMediaReceiverRegistrar.cs
deleted file mode 100644
index 43e934b53..000000000
--- a/Emby.Dlna/IMediaReceiverRegistrar.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Dlna
-{
- public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
- {
- }
-}
diff --git a/Emby.Dlna/IUpnpService.cs b/Emby.Dlna/IUpnpService.cs
deleted file mode 100644
index 9e7859567..000000000
--- a/Emby.Dlna/IUpnpService.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Threading.Tasks;
-
-namespace Emby.Dlna
-{
- public interface IUpnpService
- {
- /// <summary>
- /// Gets the content directory XML.
- /// </summary>
- /// <returns>System.String.</returns>
- string GetServiceXml();
-
- /// <summary>
- /// Processes the control request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>ControlResponse.</returns>
- Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request);
- }
-}
diff --git a/Emby.Dlna/Images/logo120.jpg b/Emby.Dlna/Images/logo120.jpg
deleted file mode 100644
index c70f4db0d..000000000
--- a/Emby.Dlna/Images/logo120.jpg
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/logo120.png b/Emby.Dlna/Images/logo120.png
deleted file mode 100644
index 14f6c8d5f..000000000
--- a/Emby.Dlna/Images/logo120.png
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/logo240.jpg b/Emby.Dlna/Images/logo240.jpg
deleted file mode 100644
index 78a27f1b5..000000000
--- a/Emby.Dlna/Images/logo240.jpg
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/logo240.png b/Emby.Dlna/Images/logo240.png
deleted file mode 100644
index ff50314d4..000000000
--- a/Emby.Dlna/Images/logo240.png
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/logo48.jpg b/Emby.Dlna/Images/logo48.jpg
deleted file mode 100644
index 269bcf589..000000000
--- a/Emby.Dlna/Images/logo48.jpg
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/logo48.png b/Emby.Dlna/Images/logo48.png
deleted file mode 100644
index d6b5fd1df..000000000
--- a/Emby.Dlna/Images/logo48.png
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/people48.jpg b/Emby.Dlna/Images/people48.jpg
deleted file mode 100644
index 3ed287062..000000000
--- a/Emby.Dlna/Images/people48.jpg
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/people48.png b/Emby.Dlna/Images/people48.png
deleted file mode 100644
index dae5f6057..000000000
--- a/Emby.Dlna/Images/people48.png
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/people480.jpg b/Emby.Dlna/Images/people480.jpg
deleted file mode 100644
index 01a316206..000000000
--- a/Emby.Dlna/Images/people480.jpg
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Images/people480.png b/Emby.Dlna/Images/people480.png
deleted file mode 100644
index 800a3d804..000000000
--- a/Emby.Dlna/Images/people480.png
+++ /dev/null
Binary files differ
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
deleted file mode 100644
index aa7012487..000000000
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ /dev/null
@@ -1,363 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-using Emby.Dlna.PlayTo;
-using Emby.Dlna.Ssdp;
-using Jellyfin.Networking.Configuration;
-using Jellyfin.Networking.Extensions;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Globalization;
-using Microsoft.Extensions.Logging;
-using Rssdp;
-using Rssdp.Infrastructure;
-
-namespace Emby.Dlna.Main
-{
- public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
- {
- private readonly IServerConfigurationManager _config;
- private readonly ILogger<DlnaEntryPoint> _logger;
- private readonly IServerApplicationHost _appHost;
- private readonly ISessionManager _sessionManager;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
- private readonly IImageProcessor _imageProcessor;
- private readonly IUserDataManager _userDataManager;
- private readonly ILocalizationManager _localization;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceDiscovery _deviceDiscovery;
- private readonly ISsdpCommunicationsServer _communicationsServer;
- private readonly INetworkManager _networkManager;
- private readonly object _syncLock = new();
- private readonly bool _disabled;
-
- private PlayToManager _manager;
- private SsdpDevicePublisher _publisher;
-
- private bool _disposed;
-
- public DlnaEntryPoint(
- IServerConfigurationManager config,
- ILoggerFactory loggerFactory,
- IServerApplicationHost appHost,
- ISessionManager sessionManager,
- IHttpClientFactory httpClientFactory,
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDlnaManager dlnaManager,
- IImageProcessor imageProcessor,
- IUserDataManager userDataManager,
- ILocalizationManager localizationManager,
- IMediaSourceManager mediaSourceManager,
- IDeviceDiscovery deviceDiscovery,
- IMediaEncoder mediaEncoder,
- ISsdpCommunicationsServer communicationsServer,
- INetworkManager networkManager)
- {
- _config = config;
- _appHost = appHost;
- _sessionManager = sessionManager;
- _httpClientFactory = httpClientFactory;
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dlnaManager = dlnaManager;
- _imageProcessor = imageProcessor;
- _userDataManager = userDataManager;
- _localization = localizationManager;
- _mediaSourceManager = mediaSourceManager;
- _deviceDiscovery = deviceDiscovery;
- _mediaEncoder = mediaEncoder;
- _communicationsServer = communicationsServer;
- _networkManager = networkManager;
- _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
-
- var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
- _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
-
- if (_disabled && _config.GetDlnaConfiguration().EnableServer)
- {
- _logger.LogError("The DLNA specification does not support HTTPS.");
- }
- }
-
- public async Task RunAsync()
- {
- await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
-
- if (_disabled)
- {
- // No use starting as dlna won't work, as we're running purely on HTTPS.
- return;
- }
-
- ReloadComponents();
-
- _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
- }
-
- private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
- {
- if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
- {
- ReloadComponents();
- }
- }
-
- private void ReloadComponents()
- {
- var options = _config.GetDlnaConfiguration();
- StartDeviceDiscovery();
-
- if (options.EnableServer)
- {
- StartDevicePublisher(options);
- }
- else
- {
- DisposeDevicePublisher();
- }
-
- if (options.EnablePlayTo)
- {
- StartPlayToManager();
- }
- else
- {
- DisposePlayToManager();
- }
- }
-
- private void StartDeviceDiscovery()
- {
- try
- {
- ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting device discovery");
- }
- }
-
- public void StartDevicePublisher(Configuration.DlnaOptions options)
- {
- if (_publisher is not null)
- {
- return;
- }
-
- try
- {
- _publisher = new SsdpDevicePublisher(
- _communicationsServer,
- Environment.OSVersion.Platform.ToString(),
- // Can not use VersionString here since that includes OS and version
- Environment.OSVersion.Version.ToString(),
- _config.GetDlnaConfiguration().SendOnlyMatchedHost)
- {
- LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
- SupportPnpRootDevice = false
- };
-
- RegisterServerEndpoints();
-
- if (options.BlastAliveMessages)
- {
- _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error registering endpoint");
- }
- }
-
- private void RegisterServerEndpoints()
- {
- var udn = CreateUuid(_appHost.SystemId);
- var descriptorUri = "/dlna/" + udn + "/description.xml";
-
- // Only get bind addresses in LAN
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses()
- .Where(x => x.Address is not null)
- .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
- .ToList();
-
- if (validInterfaces.Count == 0)
- {
- // No interfaces returned, fall back to loopback
- validInterfaces = _networkManager.GetLoopbacks().ToList();
- }
-
- foreach (var intf in validInterfaces)
- {
- var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
-
- _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
-
- var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
-
- var device = new SsdpRootDevice
- {
- CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
- Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
- Address = intf.Address,
- PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix),
- FriendlyName = "Jellyfin",
- Manufacturer = "Jellyfin",
- ModelName = "Jellyfin Server",
- Uuid = udn
- // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
- };
-
- SetProperties(device, fullService);
- _publisher.AddDevice(device);
-
- var embeddedDevices = new[]
- {
- "urn:schemas-upnp-org:service:ContentDirectory:1",
- "urn:schemas-upnp-org:service:ConnectionManager:1",
- // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
- };
-
- foreach (var subDevice in embeddedDevices)
- {
- var embeddedDevice = new SsdpEmbeddedDevice
- {
- FriendlyName = device.FriendlyName,
- Manufacturer = device.Manufacturer,
- ModelName = device.ModelName,
- Uuid = udn
- // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
- };
-
- SetProperties(embeddedDevice, subDevice);
- device.AddDevice(embeddedDevice);
- }
- }
- }
-
- private static string CreateUuid(string text)
- {
- if (!Guid.TryParse(text, out var guid))
- {
- guid = text.GetMD5();
- }
-
- return guid.ToString("D", CultureInfo.InvariantCulture);
- }
-
- private static void SetProperties(SsdpDevice device, string fullDeviceType)
- {
- var serviceParts = fullDeviceType
- .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
- .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
- .Split(':');
-
- device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
- device.DeviceClass = serviceParts[1];
- device.DeviceType = serviceParts[2];
- }
-
- private void StartPlayToManager()
- {
- lock (_syncLock)
- {
- if (_manager is not null)
- {
- return;
- }
-
- try
- {
- _manager = new PlayToManager(
- _logger,
- _sessionManager,
- _libraryManager,
- _userManager,
- _dlnaManager,
- _appHost,
- _imageProcessor,
- _deviceDiscovery,
- _httpClientFactory,
- _userDataManager,
- _localization,
- _mediaSourceManager,
- _mediaEncoder);
-
- _manager.Start();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting PlayTo manager");
- }
- }
- }
-
- private void DisposePlayToManager()
- {
- lock (_syncLock)
- {
- if (_manager is not null)
- {
- try
- {
- _logger.LogInformation("Disposing PlayToManager");
- _manager.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error disposing PlayTo manager");
- }
-
- _manager = null;
- }
- }
- }
-
- public void DisposeDevicePublisher()
- {
- if (_publisher is not null)
- {
- _logger.LogInformation("Disposing SsdpDevicePublisher");
- _publisher.Dispose();
- _publisher = null;
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- DisposeDevicePublisher();
- DisposePlayToManager();
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
deleted file mode 100644
index d8fb12742..000000000
--- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Xml;
-using Emby.Dlna.Service;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.MediaReceiverRegistrar
-{
- /// <summary>
- /// Defines the <see cref="ControlHandler" />.
- /// </summary>
- public class ControlHandler : BaseControlHandler
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="ControlHandler"/> class.
- /// </summary>
- /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
- /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
- public ControlHandler(IServerConfigurationManager config, ILogger logger)
- : base(config, logger)
- {
- }
-
- /// <inheritdoc />
- protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
- {
- if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
- {
- HandleIsAuthorized(xmlWriter);
- return;
- }
-
- if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase))
- {
- HandleIsValidated(xmlWriter);
- return;
- }
-
- throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
- }
-
- /// <summary>
- /// Records that the handle is authorized in the xml stream.
- /// </summary>
- /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
- private static void HandleIsAuthorized(XmlWriter xmlWriter)
- => xmlWriter.WriteElementString("Result", "1");
-
- /// <summary>
- /// Records that the handle is validated in the xml stream.
- /// </summary>
- /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
- private static void HandleIsValidated(XmlWriter xmlWriter)
- => xmlWriter.WriteElementString("Result", "1");
- }
-}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
deleted file mode 100644
index a5aae515c..000000000
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Net.Http;
-using System.Threading.Tasks;
-using Emby.Dlna.Service;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.MediaReceiverRegistrar
-{
- /// <summary>
- /// Defines the <see cref="MediaReceiverRegistrarService" />.
- /// </summary>
- public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
- {
- private readonly IServerConfigurationManager _config;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
- /// </summary>
- /// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
- /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
- /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
- public MediaReceiverRegistrarService(
- ILogger<MediaReceiverRegistrarService> logger,
- IHttpClientFactory httpClientFactory,
- IServerConfigurationManager config)
- : base(logger, httpClientFactory)
- {
- _config = config;
- }
-
- /// <inheritdoc />
- public string GetServiceXml()
- {
- return MediaReceiverRegistrarXmlBuilder.GetXml();
- }
-
- /// <inheritdoc />
- public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
- {
- return new ControlHandler(
- _config,
- Logger)
- .ProcessControlRequestAsync(request);
- }
- }
-}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
deleted file mode 100644
index f3789a791..000000000
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System.Collections.Generic;
-using Emby.Dlna.Common;
-using Emby.Dlna.Service;
-
-namespace Emby.Dlna.MediaReceiverRegistrar
-{
- /// <summary>
- /// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
- /// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
- /// </summary>
- public static class MediaReceiverRegistrarXmlBuilder
- {
- /// <summary>
- /// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
- /// </summary>
- /// <returns>An XML representation of this service.</returns>
- public static string GetXml()
- {
- return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
- }
-
- /// <summary>
- /// The a list of all the state variables for this invocation.
- /// </summary>
- /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
- private static IEnumerable<StateVariable> GetStateVariables()
- {
- var list = new List<StateVariable>
- {
- new StateVariable
- {
- Name = "AuthorizationGrantedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_DeviceID",
- DataType = "string",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "AuthorizationDeniedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- },
-
- new StateVariable
- {
- Name = "ValidationSucceededUpdateID",
- DataType = "ui4",
- SendsEvents = true
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_RegistrationRespMsg",
- DataType = "bin.base64",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_RegistrationReqMsg",
- DataType = "bin.base64",
- SendsEvents = false
- },
-
- new StateVariable
- {
- Name = "ValidationRevokedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- },
-
- new StateVariable
- {
- Name = "A_ARG_TYPE_Result",
- DataType = "int",
- SendsEvents = false
- }
- };
-
- return list;
- }
- }
-}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
deleted file mode 100644
index 56788ae22..000000000
--- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
+++ /dev/null
@@ -1,187 +0,0 @@
-using System.Collections.Generic;
-using Emby.Dlna.Common;
-
-namespace Emby.Dlna.MediaReceiverRegistrar
-{
- /// <summary>
- /// Defines the <see cref="ServiceActionListBuilder" />.
- /// </summary>
- public static class ServiceActionListBuilder
- {
- /// <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[]
- {
- GetIsValidated(),
- GetIsAuthorized(),
- GetRegisterDevice(),
- GetGetAuthorizationDeniedUpdateID(),
- GetGetAuthorizationGrantedUpdateID(),
- GetGetValidationRevokedUpdateID(),
- GetGetValidationSucceededUpdateID()
- };
- }
-
- /// <summary>
- /// Returns the action details for "IsValidated".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetIsValidated()
- {
- var action = new ServiceAction
- {
- Name = "IsValidated"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "DeviceID",
- Direction = "in"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Result",
- Direction = "out"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "IsAuthorized".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetIsAuthorized()
- {
- var action = new ServiceAction
- {
- Name = "IsAuthorized"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "DeviceID",
- Direction = "in"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "Result",
- Direction = "out"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "RegisterDevice".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetRegisterDevice()
- {
- var action = new ServiceAction
- {
- Name = "RegisterDevice"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RegistrationReqMsg",
- Direction = "in"
- });
-
- action.ArgumentList.Add(new Argument
- {
- Name = "RegistrationRespMsg",
- Direction = "out"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetValidationSucceededUpdateID".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetGetValidationSucceededUpdateID()
- {
- var action = new ServiceAction
- {
- Name = "GetValidationSucceededUpdateID"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ValidationSucceededUpdateID",
- Direction = "out"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetGetAuthorizationDeniedUpdateID()
- {
- var action = new ServiceAction
- {
- Name = "GetAuthorizationDeniedUpdateID"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "AuthorizationDeniedUpdateID",
- Direction = "out"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetValidationRevokedUpdateID".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetGetValidationRevokedUpdateID()
- {
- var action = new ServiceAction
- {
- Name = "GetValidationRevokedUpdateID"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "ValidationRevokedUpdateID",
- Direction = "out"
- });
-
- return action;
- }
-
- /// <summary>
- /// Returns the action details for "GetAuthorizationGrantedUpdateID".
- /// </summary>
- /// <returns>The <see cref="ServiceAction"/>.</returns>
- private static ServiceAction GetGetAuthorizationGrantedUpdateID()
- {
- var action = new ServiceAction
- {
- Name = "GetAuthorizationGrantedUpdateID"
- };
-
- action.ArgumentList.Add(new Argument
- {
- Name = "AuthorizationGrantedUpdateID",
- Direction = "out"
- });
-
- return action;
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
deleted file mode 100644
index d21cc6913..000000000
--- a/Emby.Dlna/PlayTo/Device.cs
+++ /dev/null
@@ -1,1269 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Security;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Xml;
-using System.Xml.Linq;
-using Emby.Dlna.Common;
-using Emby.Dlna.Ssdp;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.PlayTo
-{
- public class Device : IDisposable
- {
- private readonly IHttpClientFactory _httpClientFactory;
-
- private readonly ILogger _logger;
-
- private readonly object _timerLock = new object();
- private Timer? _timer;
- private int _muteVol;
- private int _volume;
- private DateTime _lastVolumeRefresh;
- private bool _volumeRefreshActive;
- private int _connectFailureCount;
- private bool _disposed;
-
- public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger)
- {
- Properties = deviceProperties;
- _httpClientFactory = httpClientFactory;
- _logger = logger;
- }
-
- public event EventHandler<PlaybackStartEventArgs>? PlaybackStart;
-
- public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress;
-
- public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped;
-
- public event EventHandler<MediaChangedEventArgs>? MediaChanged;
-
- public DeviceInfo Properties { get; set; }
-
- public bool IsMuted { get; set; }
-
- public int Volume
- {
- get
- {
- RefreshVolumeIfNeeded().GetAwaiter().GetResult();
- return _volume;
- }
-
- set => _volume = value;
- }
-
- public TimeSpan? Duration { get; set; }
-
- public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
-
- public TransportState TransportState { get; private set; }
-
- public bool IsPlaying => TransportState == TransportState.PLAYING;
-
- public bool IsPaused => TransportState == TransportState.PAUSED_PLAYBACK;
-
- public bool IsStopped => TransportState == TransportState.STOPPED;
-
- public Action? OnDeviceUnavailable { get; set; }
-
- private TransportCommands? AvCommands { get; set; }
-
- private TransportCommands? RendererCommands { get; set; }
-
- public UBaseObject? CurrentMediaInfo { get; private set; }
-
- public void Start()
- {
- _logger.LogDebug("Dlna Device.Start");
- _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
- }
-
- private Task RefreshVolumeIfNeeded()
- {
- if (_volumeRefreshActive
- && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
- {
- _lastVolumeRefresh = DateTime.UtcNow;
- return RefreshVolume();
- }
-
- return Task.CompletedTask;
- }
-
- private async Task RefreshVolume(CancellationToken cancellationToken = default)
- {
- if (_disposed)
- {
- return;
- }
-
- try
- {
- await GetVolume(cancellationToken).ConfigureAwait(false);
- await GetMute(cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error updating device volume info for {DeviceName}", Properties.Name);
- }
- }
-
- private void RestartTimer(bool immediate = false)
- {
- lock (_timerLock)
- {
- if (_disposed)
- {
- return;
- }
-
- _volumeRefreshActive = true;
-
- var time = immediate ? 100 : 10000;
- _timer?.Change(time, Timeout.Infinite);
- }
- }
-
- /// <summary>
- /// Restarts the timer in inactive mode.
- /// </summary>
- private void RestartTimerInactive()
- {
- lock (_timerLock)
- {
- if (_disposed)
- {
- return;
- }
-
- _volumeRefreshActive = false;
-
- _timer?.Change(Timeout.Infinite, Timeout.Infinite);
- }
- }
-
- public Task VolumeDown(CancellationToken cancellationToken)
- {
- var sendVolume = Math.Max(Volume - 5, 0);
-
- return SetVolume(sendVolume, cancellationToken);
- }
-
- public Task VolumeUp(CancellationToken cancellationToken)
- {
- var sendVolume = Math.Min(Volume + 5, 100);
-
- return SetVolume(sendVolume, cancellationToken);
- }
-
- public Task ToggleMute(CancellationToken cancellationToken)
- {
- if (IsMuted)
- {
- return Unmute(cancellationToken);
- }
-
- return Mute(cancellationToken);
- }
-
- public async Task Mute(CancellationToken cancellationToken)
- {
- var success = await SetMute(true, cancellationToken).ConfigureAwait(true);
-
- if (!success)
- {
- await SetVolume(0, cancellationToken).ConfigureAwait(false);
- }
- }
-
- public async Task Unmute(CancellationToken cancellationToken)
- {
- var success = await SetMute(false, cancellationToken).ConfigureAwait(true);
-
- if (!success)
- {
- var sendVolume = _muteVol <= 0 ? 20 : _muteVol;
-
- await SetVolume(sendVolume, cancellationToken).ConfigureAwait(false);
- }
- }
-
- private DeviceService? GetServiceRenderingControl()
- {
- var services = Properties.Services;
-
- return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:RenderingControl:1", StringComparison.OrdinalIgnoreCase)) ??
- services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
- }
-
- private DeviceService? GetAvTransportService()
- {
- var services = Properties.Services;
-
- return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:AVTransport:1", StringComparison.OrdinalIgnoreCase)) ??
- services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:AVTransport", StringComparison.OrdinalIgnoreCase));
- }
-
- private async Task<bool> SetMute(bool mute, CancellationToken cancellationToken)
- {
- var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
- if (command is null)
- {
- return false;
- }
-
- var service = GetServiceRenderingControl();
-
- if (service is null)
- {
- return false;
- }
-
- _logger.LogDebug("Setting mute");
- var value = mute ? 1 : 0;
-
- await new DlnaHttpClient(_logger, _httpClientFactory)
- .SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
- cancellationToken: cancellationToken)
- .ConfigureAwait(false);
-
- IsMuted = mute;
-
- return true;
- }
-
- /// <summary>
- /// Sets volume on a scale of 0-100.
- /// </summary>
- /// <param name="value">The volume on a scale of 0-100.</param>
- /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
- /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
- public async Task SetVolume(int value, CancellationToken cancellationToken)
- {
- var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
- if (command is null)
- {
- return;
- }
-
- var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service");
-
- // Set it early and assume it will succeed
- // Remote control will perform better
- Volume = value;
-
- await new DlnaHttpClient(_logger, _httpClientFactory)
- .SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
- cancellationToken: cancellationToken)
- .ConfigureAwait(false);
- }
-
- public async Task Seek(TimeSpan value, CancellationToken cancellationToken)
- {
- var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
- if (command is null)
- {
- return;
- }
-
- var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
- await new DlnaHttpClient(_logger, _httpClientFactory)
- .SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above
- cancellationToken: cancellationToken)
- .ConfigureAwait(false);
-
- RestartTimer(true);
- }
-
- public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken)
- {
- var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- url = url.Replace("&", "&amp;", StringComparison.Ordinal);
-
- _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
-
- var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
- if (command is null)
- {
- return;
- }
-
- var dictionary = new Dictionary<string, string>
- {
- { "CurrentURI", url },
- { "CurrentURIMetaData", CreateDidlMeta(metaData) }
- };
-
- var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
- var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
- await new DlnaHttpClient(_logger, _httpClientFactory)
- .SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- post,
- header: header,
- cancellationToken: cancellationToken)
- .ConfigureAwait(false);
-
- await Task.Delay(50, cancellationToken).ConfigureAwait(false);
-
- try
- {
- await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
- }
- catch
- {
- // Some devices will throw an error if you tell it to play when it's already playing
- // Others won't
- }
-
- RestartTimer(true);
- }
-
- /*
- * SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
- * Without that information, the next track command on the device does not work.
- */
- public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default)
- {
- var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- url = url.Replace("&", "&amp;", StringComparison.Ordinal);
-
- _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
-
- var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
- if (command is null)
- {
- return;
- }
-
- var dictionary = new Dictionary<string, string>
- {
- { "NextURI", url },
- { "NextURIMetaData", CreateDidlMeta(metaData) }
- };
-
- var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
- var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
- await new DlnaHttpClient(_logger, _httpClientFactory)
- .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
- .ConfigureAwait(false);
- }
-
- private static string CreateDidlMeta(string value)
- {
- if (string.IsNullOrEmpty(value))
- {
- return string.Empty;
- }
-
- return SecurityElement.Escape(value);
- }
-
- private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
- {
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play");
- if (command is null)
- {
- return Task.CompletedTask;
- }
-
- var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
- return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- avCommands.BuildPost(command, service.ServiceType, 1),
- cancellationToken: cancellationToken);
- }
-
- public async Task SetPlay(CancellationToken cancellationToken)
- {
- var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
- if (avCommands is null)
- {
- return;
- }
-
- await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
-
- RestartTimer(true);
- }
-
- public async Task SetStop(CancellationToken cancellationToken)
- {
- var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
- if (command is null)
- {
- return;
- }
-
- var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
- await new DlnaHttpClient(_logger, _httpClientFactory)
- .SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
- cancellationToken: cancellationToken)
- .ConfigureAwait(false);
-
- RestartTimer(true);
- }
-
- public async Task SetPause(CancellationToken cancellationToken)
- {
- var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
- if (command is null)
- {
- return;
- }
-
- var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
- await new DlnaHttpClient(_logger, _httpClientFactory)
- .SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
- cancellationToken: cancellationToken)
- .ConfigureAwait(false);
-
- TransportState = TransportState.PAUSED_PLAYBACK;
-
- RestartTimer(true);
- }
-
- private async void TimerCallback(object? sender)
- {
- if (_disposed)
- {
- return;
- }
-
- try
- {
- var cancellationToken = CancellationToken.None;
-
- var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- if (avCommands is null)
- {
- return;
- }
-
- var transportState = await GetTransportInfo(avCommands, cancellationToken).ConfigureAwait(false);
-
- if (_disposed)
- {
- return;
- }
-
- if (transportState.HasValue)
- {
- // If we're not playing anything no need to get additional data
- if (transportState.Value == TransportState.STOPPED)
- {
- UpdateMediaInfo(null, transportState.Value);
- }
- else
- {
- var tuple = await GetPositionInfo(avCommands, cancellationToken).ConfigureAwait(false);
-
- var currentObject = tuple.Track;
-
- if (tuple.Success && currentObject is null)
- {
- currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false);
- }
-
- if (currentObject is not null)
- {
- UpdateMediaInfo(currentObject, transportState.Value);
- }
- }
-
- _connectFailureCount = 0;
-
- if (_disposed)
- {
- return;
- }
-
- // 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();
- }
- else
- {
- RestartTimer();
- }
- }
- else
- {
- RestartTimerInactive();
- }
- }
- catch (Exception ex)
- {
- if (_disposed)
- {
- return;
- }
-
- _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
-
- _connectFailureCount++;
-
- if (_connectFailureCount >= 3)
- {
- var action = OnDeviceUnavailable;
- if (action is not null)
- {
- _logger.LogDebug("Disposing device due to loss of connection");
- action();
- return;
- }
- }
-
- RestartTimerInactive();
- }
- }
-
- private async Task GetVolume(CancellationToken cancellationToken)
- {
- if (_disposed)
- {
- return;
- }
-
- var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
- if (command is null)
- {
- return;
- }
-
- var service = GetServiceRenderingControl();
-
- if (service is null)
- {
- return;
- }
-
- var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- if (result is null || result.Document is null)
- {
- return;
- }
-
- var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i is not null);
- var volumeValue = volume?.Value;
-
- if (string.IsNullOrWhiteSpace(volumeValue))
- {
- return;
- }
-
- Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
-
- if (Volume > 0)
- {
- _muteVol = Volume;
- }
- }
-
- private async Task GetMute(CancellationToken cancellationToken)
- {
- if (_disposed)
- {
- return;
- }
-
- var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
- if (command is null)
- {
- return;
- }
-
- var service = GetServiceRenderingControl();
-
- if (service is null)
- {
- return;
- }
-
- var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- if (result is null || result.Document is null)
- {
- return;
- }
-
- var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
- .Select(i => i.Element("CurrentMute"))
- .FirstOrDefault(i => i is not null);
-
- IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
- }
-
- private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
- {
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
- if (command is null)
- {
- return null;
- }
-
- var service = GetAvTransportService();
- if (service is null)
- {
- return null;
- }
-
- var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- avCommands.BuildPost(command, service.ServiceType),
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- if (result is null || result.Document is null)
- {
- return null;
- }
-
- var transportState =
- result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i is not null);
-
- var transportStateValue = transportState?.Value;
-
- if (transportStateValue is not null
- && Enum.TryParse(transportStateValue, true, out TransportState state))
- {
- return state;
- }
-
- return null;
- }
-
- private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
- {
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
- if (command is null)
- {
- return null;
- }
-
- var service = GetAvTransportService();
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
- var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- if (rendererCommands is null)
- {
- return null;
- }
-
- var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- rendererCommands.BuildPost(command, service.ServiceType),
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- if (result is null || result.Document is null)
- {
- return null;
- }
-
- var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault();
-
- if (track is null)
- {
- return null;
- }
-
- var e = track.Element(UPnpNamespaces.Items) ?? track;
-
- var elementString = (string)e;
-
- if (!string.IsNullOrWhiteSpace(elementString))
- {
- return UpnpContainer.Create(e);
- }
-
- track = result.Document.Descendants("CurrentURI").FirstOrDefault();
-
- if (track is null)
- {
- return null;
- }
-
- e = track.Element(UPnpNamespaces.Items) ?? track;
-
- elementString = (string)e;
-
- if (!string.IsNullOrWhiteSpace(elementString))
- {
- return new UBaseObject
- {
- Url = elementString
- };
- }
-
- return null;
- }
-
- private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
- {
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
- if (command is null)
- {
- return (false, null);
- }
-
- var service = GetAvTransportService();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
- var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
-
- if (rendererCommands is null)
- {
- return (false, null);
- }
-
- var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
- Properties.BaseUrl,
- service,
- command.Name,
- rendererCommands.BuildPost(command, service.ServiceType),
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- if (result is null || result.Document is null)
- {
- return (false, null);
- }
-
- var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i is not null);
- var trackUri = trackUriElem?.Value;
-
- var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i is not null);
- var duration = durationElem?.Value;
-
- if (!string.IsNullOrWhiteSpace(duration)
- && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
- {
- Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
- }
- else
- {
- Duration = null;
- }
-
- var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i is not null);
- var position = positionElem?.Value;
-
- if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
- {
- Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
- }
-
- var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
-
- if (track is null)
- {
- // If track is null, some vendors do this, use GetMediaInfo instead.
- return (true, null);
- }
-
- var trackString = (string)track;
-
- if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
- {
- return (true, null);
- }
-
- XElement? uPnpResponse = null;
-
- try
- {
- uPnpResponse = ParseResponse(trackString);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Uncaught exception while parsing xml");
- }
-
- if (uPnpResponse is null)
- {
- _logger.LogError("Failed to parse xml: \n {Xml}", trackString);
- return (true, null);
- }
-
- var e = uPnpResponse.Element(UPnpNamespaces.Items);
-
- var uTrack = CreateUBaseObject(e, trackUri);
-
- return (true, uTrack);
- }
-
- private XElement? ParseResponse(string xml)
- {
- // Handle different variations sent back by devices.
- try
- {
- return XElement.Parse(xml);
- }
- catch (XmlException)
- {
- }
-
- // 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>")
- .Descendants()
- .First();
- }
- catch (XmlException)
- {
- }
-
- // some devices send back invalid xml
- try
- {
- return XElement.Parse(xml.Replace("&", "&amp;", StringComparison.Ordinal));
- }
- catch (XmlException)
- {
- }
-
- return null;
- }
-
- private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri)
- {
- ArgumentNullException.ThrowIfNull(container);
-
- var url = container.GetValue(UPnpNamespaces.Res);
-
- if (string.IsNullOrWhiteSpace(url))
- {
- url = trackUri;
- }
-
- return new UBaseObject
- {
- Id = container.GetAttributeValue(UPnpNamespaces.Id),
- ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
- Title = container.GetValue(UPnpNamespaces.Title),
- IconUrl = container.GetValue(UPnpNamespaces.Artwork),
- SecondText = string.Empty,
- Url = url,
- ProtocolInfo = GetProtocolInfo(container),
- MetaData = container.ToString()
- };
- }
-
- private static string[] GetProtocolInfo(XElement container)
- {
- ArgumentNullException.ThrowIfNull(container);
-
- var resElement = container.Element(UPnpNamespaces.Res);
-
- if (resElement is not null)
- {
- var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
-
- if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
- {
- return info.Value.Split(':');
- }
- }
-
- return new string[4];
- }
-
- private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken)
- {
- if (AvCommands is not null)
- {
- return AvCommands;
- }
-
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
-
- var avService = GetAvTransportService();
- if (avService is null)
- {
- return null;
- }
-
- string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
-
- var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
-
- var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
- if (document is null)
- {
- return null;
- }
-
- AvCommands = TransportCommands.Create(document);
- return AvCommands;
- }
-
- private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken)
- {
- if (RendererCommands is not null)
- {
- return RendererCommands;
- }
-
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
-
- var avService = GetServiceRenderingControl();
- ArgumentNullException.ThrowIfNull(avService);
-
- string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
-
- var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
- _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
- var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
- if (document is null)
- {
- return null;
- }
-
- RendererCommands = TransportCommands.Create(document);
- return RendererCommands;
- }
-
- private string NormalizeUrl(string baseUrl, string url)
- {
- // If it's already a complete url, don't stick anything onto the front of it
- if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return url;
- }
-
- if (!url.Contains('/', StringComparison.Ordinal))
- {
- url = "/dmr/" + url;
- }
-
- if (!url.StartsWith('/'))
- {
- url = "/" + url;
- }
-
- return baseUrl + url;
- }
-
- public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
- {
- var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
-
- var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
- if (document is null)
- {
- return null;
- }
-
- var friendlyNames = new List<string>();
-
- var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
- if (name is not null && !string.IsNullOrWhiteSpace(name.Value))
- {
- friendlyNames.Add(name.Value);
- }
-
- var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
- if (room is not null && !string.IsNullOrWhiteSpace(room.Value))
- {
- friendlyNames.Add(room.Value);
- }
-
- var deviceProperties = new DeviceInfo()
- {
- Name = string.Join(' ', friendlyNames),
- BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
- };
-
- var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault();
- if (model is not null)
- {
- deviceProperties.ModelName = model.Value;
- }
-
- var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
- if (modelNumber is not null)
- {
- deviceProperties.ModelNumber = modelNumber.Value;
- }
-
- var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
- if (uuid is not null)
- {
- deviceProperties.UUID = uuid.Value;
- }
-
- var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
- if (manufacturer is not null)
- {
- deviceProperties.Manufacturer = manufacturer.Value;
- }
-
- var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
- if (manufacturerUrl is not null)
- {
- deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
- }
-
- var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
- if (presentationUrl is not null)
- {
- deviceProperties.PresentationUrl = presentationUrl.Value;
- }
-
- var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
- if (modelUrl is not null)
- {
- deviceProperties.ModelUrl = modelUrl.Value;
- }
-
- var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
- if (serialNumber is not null)
- {
- deviceProperties.SerialNumber = serialNumber.Value;
- }
-
- var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
- if (modelDescription is not null)
- {
- deviceProperties.ModelDescription = modelDescription.Value;
- }
-
- var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
- if (icon is not null)
- {
- deviceProperties.Icon = CreateIcon(icon);
- }
-
- foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
- {
- if (services is null)
- {
- continue;
- }
-
- var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
- if (servicesList is null)
- {
- continue;
- }
-
- foreach (var element in servicesList)
- {
- var service = Create(element);
-
- if (service is not null)
- {
- deviceProperties.Services.Add(service);
- }
- }
- }
-
- return new Device(deviceProperties, httpClientFactory, logger);
- }
-
-#nullable enable
- private static DeviceIcon CreateIcon(XElement element)
- {
- ArgumentNullException.ThrowIfNull(element);
-
- var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
- var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
-
- _ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue);
- _ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue);
-
- return new DeviceIcon
- {
- Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty,
- Height = heightValue,
- MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty,
- Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty,
- Width = widthValue
- };
- }
-
- private static DeviceService Create(XElement element)
- => new DeviceService()
- {
- ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty,
- EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty,
- ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty,
- ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty,
- ServiceType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")) ?? string.Empty
- };
-
- private void UpdateMediaInfo(UBaseObject? mediaInfo, TransportState state)
- {
- TransportState = state;
-
- var previousMediaInfo = CurrentMediaInfo;
- CurrentMediaInfo = mediaInfo;
-
- if (mediaInfo is null)
- {
- if (previousMediaInfo is not null)
- {
- OnPlaybackStop(previousMediaInfo);
- }
- }
- else if (previousMediaInfo is null)
- {
- if (state != TransportState.STOPPED)
- {
- OnPlaybackStart(mediaInfo);
- }
- }
- else if (mediaInfo.Equals(previousMediaInfo))
- {
- OnPlaybackProgress(mediaInfo);
- }
- else
- {
- OnMediaChanged(previousMediaInfo, mediaInfo);
- }
- }
-
- private void OnPlaybackStart(UBaseObject mediaInfo)
- {
- if (string.IsNullOrWhiteSpace(mediaInfo.Url))
- {
- return;
- }
-
- PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
- }
-
- private void OnPlaybackProgress(UBaseObject mediaInfo)
- {
- if (string.IsNullOrWhiteSpace(mediaInfo.Url))
- {
- return;
- }
-
- PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
- }
-
- private void OnPlaybackStop(UBaseObject mediaInfo)
- {
- PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
- }
-
- private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
- {
- MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and optionally managed resources.
- /// </summary>
- /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _timer?.Dispose();
- }
-
- _timer = null;
- Properties = null!;
-
- _disposed = true;
- }
-
- /// <inheritdoc />
- public override string ToString()
- {
- return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs
deleted file mode 100644
index 2acfff4eb..000000000
--- a/Emby.Dlna/PlayTo/DeviceInfo.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using Emby.Dlna.Common;
-using MediaBrowser.Model.Dlna;
-
-namespace Emby.Dlna.PlayTo
-{
- public class DeviceInfo
- {
- private readonly List<DeviceService> _services = new List<DeviceService>();
- private string _baseUrl = string.Empty;
-
- public DeviceInfo()
- {
- Name = "Generic Device";
- }
-
- public string UUID { get; set; }
-
- public string Name { get; set; }
-
- public string ModelName { get; set; }
-
- public string ModelNumber { get; set; }
-
- public string ModelDescription { get; set; }
-
- public string ModelUrl { get; set; }
-
- public string Manufacturer { get; set; }
-
- public string SerialNumber { get; set; }
-
- public string ManufacturerUrl { get; set; }
-
- public string PresentationUrl { get; set; }
-
- public string BaseUrl
- {
- get => _baseUrl;
- set => _baseUrl = value;
- }
-
- public DeviceIcon Icon { get; set; }
-
- public List<DeviceService> Services => _services;
-
- public DeviceIdentification ToDeviceIdentification()
- {
- return new DeviceIdentification
- {
- Manufacturer = Manufacturer,
- ModelName = ModelName,
- ModelNumber = ModelNumber,
- FriendlyName = Name,
- ManufacturerUrl = ManufacturerUrl,
- ModelUrl = ModelUrl,
- ModelDescription = ModelDescription,
- SerialNumber = SerialNumber
- };
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
deleted file mode 100644
index 255c51f19..000000000
--- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Net.Http;
-using System.Net.Mime;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Xml;
-using System.Xml.Linq;
-using Emby.Dlna.Common;
-using MediaBrowser.Common.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.PlayTo
-{
- /// <summary>
- /// Http client for Dlna PlayTo function.
- /// </summary>
- public partial class DlnaHttpClient
- {
- private readonly ILogger _logger;
- private readonly IHttpClientFactory _httpClientFactory;
-
- public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory)
- {
- _logger = logger;
- _httpClientFactory = httpClientFactory;
- }
-
- [GeneratedRegex("(&(?![a-z]*;))")]
- private static partial Regex EscapeAmpersandRegex();
-
- private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
- {
- // If it's already a complete url, don't stick anything onto the front of it
- if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return serviceUrl;
- }
-
- if (!serviceUrl.StartsWith('/'))
- {
- serviceUrl = "/" + serviceUrl;
- }
-
- return baseUrl + serviceUrl;
- }
-
- private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
- using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (stream.ConfigureAwait(false))
- {
- try
- {
- return await XDocument.LoadAsync(
- stream,
- LoadOptions.None,
- cancellationToken).ConfigureAwait(false);
- }
- catch (XmlException)
- {
- // try correcting the Xml response with common errors
- stream.Position = 0;
- using StreamReader sr = new StreamReader(stream);
- var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
-
- // find and replace unescaped ampersands (&)
- xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
-
- try
- {
- // retry reading Xml
- using var xmlReader = new StringReader(xmlString);
- return await XDocument.LoadAsync(
- xmlReader,
- LoadOptions.None,
- cancellationToken).ConfigureAwait(false);
- }
- catch (XmlException ex)
- {
- _logger.LogError(ex, "Failed to parse response");
- _logger.LogDebug("Malformed response: {Content}\n", xmlString);
-
- return null;
- }
- }
- }
- }
-
- public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken)
- {
- using var request = new HttpRequestMessage(HttpMethod.Get, url);
-
- // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
- return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
- }
-
- public async Task<XDocument?> SendCommandAsync(
- string baseUrl,
- DeviceService service,
- string command,
- string postData,
- string? header = null,
- CancellationToken cancellationToken = default)
- {
- using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl))
- {
- Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml)
- };
-
- request.Headers.TryAddWithoutValidation(
- "SOAPACTION",
- string.Format(
- CultureInfo.InvariantCulture,
- "\"{0}#{1}\"",
- service.ServiceType,
- command));
- request.Headers.Pragma.ParseAdd("no-cache");
-
- if (!string.IsNullOrEmpty(header))
- {
- request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
- }
-
- // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
- return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
deleted file mode 100644
index 0f7a524d6..000000000
--- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Dlna.PlayTo
-{
- public class MediaChangedEventArgs : EventArgs
- {
- public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
- {
- OldMediaInfo = oldMediaInfo;
- NewMediaInfo = newMediaInfo;
- }
-
- public UBaseObject OldMediaInfo { get; set; }
-
- public UBaseObject NewMediaInfo { get; set; }
- }
-}
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
deleted file mode 100644
index b1ad15cdc..000000000
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ /dev/null
@@ -1,980 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Dlna.Didl;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Events;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.WebUtilities;
-using Microsoft.Extensions.Logging;
-using Photo = MediaBrowser.Controller.Entities.Photo;
-
-namespace Emby.Dlna.PlayTo
-{
- public class PlayToController : ISessionController, IDisposable
- {
- private readonly SessionInfo _session;
- private readonly ISessionManager _sessionManager;
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger _logger;
- private readonly IDlnaManager _dlnaManager;
- private readonly IUserManager _userManager;
- private readonly IImageProcessor _imageProcessor;
- private readonly IUserDataManager _userDataManager;
- private readonly ILocalizationManager _localization;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IMediaEncoder _mediaEncoder;
-
- private readonly IDeviceDiscovery _deviceDiscovery;
- private readonly string _serverAddress;
- private readonly string? _accessToken;
-
- private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
- private Device _device;
- private int _currentPlaylistIndex;
-
- private bool _disposed;
-
- public PlayToController(
- SessionInfo session,
- ISessionManager sessionManager,
- ILibraryManager libraryManager,
- ILogger logger,
- IDlnaManager dlnaManager,
- IUserManager userManager,
- IImageProcessor imageProcessor,
- string serverAddress,
- string? accessToken,
- IDeviceDiscovery deviceDiscovery,
- IUserDataManager userDataManager,
- ILocalizationManager localization,
- IMediaSourceManager mediaSourceManager,
- IMediaEncoder mediaEncoder,
- Device device)
- {
- _session = session;
- _sessionManager = sessionManager;
- _libraryManager = libraryManager;
- _logger = logger;
- _dlnaManager = dlnaManager;
- _userManager = userManager;
- _imageProcessor = imageProcessor;
- _serverAddress = serverAddress;
- _accessToken = accessToken;
- _deviceDiscovery = deviceDiscovery;
- _userDataManager = userDataManager;
- _localization = localization;
- _mediaSourceManager = mediaSourceManager;
- _mediaEncoder = mediaEncoder;
-
- _device = device;
- _device.OnDeviceUnavailable = OnDeviceUnavailable;
- _device.PlaybackStart += OnDevicePlaybackStart;
- _device.PlaybackProgress += OnDevicePlaybackProgress;
- _device.PlaybackStopped += OnDevicePlaybackStopped;
- _device.MediaChanged += OnDeviceMediaChanged;
-
- _device.Start();
-
- _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
- }
-
- public bool IsSessionActive => !_disposed;
-
- public bool SupportsMediaControl => IsSessionActive;
-
- /*
- * Send a message to the DLNA device to notify what is the next track in the playlist.
- */
- private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
- {
- if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
- {
- // The current playing item is indeed in the play list and we are not yet at the end of the playlist.
- var nextItemIndex = currentPlayListItemIndex + 1;
- var nextItem = _playlist[nextItemIndex];
-
- // Send the SetNextAvTransport message.
- await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
- }
- }
-
- private void OnDeviceUnavailable()
- {
- try
- {
- _sessionManager.ReportSessionEnded(_session.Id);
- }
- catch (Exception ex)
- {
- // Could throw if the session is already gone
- _logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
- }
- }
-
- private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
- {
- var info = e.Argument;
-
- if (!_disposed
- && info.Headers.TryGetValue("USN", out string? usn)
- && usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
- && (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
- || (info.Headers.TryGetValue("NT", out string? nt)
- && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
- {
- OnDeviceUnavailable();
- }
- }
-
- private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
- {
- if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
- {
- return;
- }
-
- try
- {
- var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager);
- if (streamInfo.Item is not null)
- {
- var positionTicks = GetProgressPositionTicks(streamInfo);
-
- await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
- }
-
- streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
- if (streamInfo.Item is null)
- {
- return;
- }
-
- var newItemProgress = GetProgressInfo(streamInfo);
-
- await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
-
- // Send a message to the DLNA device to notify what is the next track in the playlist.
- var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId.Equals(streamInfo.ItemId));
- if (currentItemIndex >= 0)
- {
- _currentPlaylistIndex = currentItemIndex;
- }
-
- await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reporting progress");
- }
- }
-
- private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
- {
- if (_disposed)
- {
- return;
- }
-
- try
- {
- var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
-
- if (streamInfo.Item is null)
- {
- return;
- }
-
- var positionTicks = GetProgressPositionTicks(streamInfo);
-
- await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
-
- var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
-
- var duration = mediaSource is null
- ? _device.Duration?.Ticks
- : mediaSource.RunTimeTicks;
-
- var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
-
- if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
- {
- double percent = positionTicks.Value;
- percent /= duration.Value;
-
- playedToCompletion = Math.Abs(1 - percent) <= .1;
- }
-
- if (playedToCompletion)
- {
- await SetPlaylistIndex(_currentPlaylistIndex + 1).ConfigureAwait(false);
- }
- else
- {
- _playlist.Clear();
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reporting playback stopped");
- }
- }
-
- private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
- {
- try
- {
- await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo
- {
- ItemId = streamInfo.ItemId,
- SessionId = _session.Id,
- PositionTicks = positionTicks,
- MediaSourceId = streamInfo.MediaSourceId
- }).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reporting progress");
- }
- }
-
- private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
- {
- if (_disposed)
- {
- return;
- }
-
- try
- {
- var info = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
-
- if (info.Item is not null)
- {
- var progress = GetProgressInfo(info);
-
- await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reporting progress");
- }
- }
-
- private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
- {
- if (_disposed)
- {
- return;
- }
-
- try
- {
- var mediaUrl = e.MediaInfo.Url;
-
- if (string.IsNullOrWhiteSpace(mediaUrl))
- {
- return;
- }
-
- var info = StreamParams.ParseFromUrl(mediaUrl, _libraryManager, _mediaSourceManager);
-
- if (info.Item is not null)
- {
- var progress = GetProgressInfo(info);
-
- await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reporting progress");
- }
- }
-
- private long? GetProgressPositionTicks(StreamParams info)
- {
- var ticks = _device.Position.Ticks;
-
- if (!EnableClientSideSeek(info))
- {
- ticks += info.StartPositionTicks;
- }
-
- return ticks;
- }
-
- private PlaybackStartInfo GetProgressInfo(StreamParams info)
- {
- return new PlaybackStartInfo
- {
- ItemId = info.ItemId,
- SessionId = _session.Id,
- PositionTicks = GetProgressPositionTicks(info),
- IsMuted = _device.IsMuted,
- IsPaused = _device.IsPaused,
- MediaSourceId = info.MediaSourceId,
- AudioStreamIndex = info.AudioStreamIndex,
- SubtitleStreamIndex = info.SubtitleStreamIndex,
- VolumeLevel = _device.Volume,
-
- CanSeek = true,
-
- PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode
- };
- }
-
- public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
- {
- _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
-
- var user = command.ControllingUserId.Equals(default)
- ? null :
- _userManager.GetUserById(command.ControllingUserId);
-
- var items = new List<BaseItem>();
- foreach (var id in command.ItemIds)
- {
- AddItemFromId(id, items);
- }
-
- var startIndex = command.StartIndex ?? 0;
- int len = items.Count - startIndex;
- if (startIndex > 0)
- {
- items = items.GetRange(startIndex, len);
- }
-
- var playlist = new PlaylistItem[len];
-
- // Not nullable enabled - so this is required.
- playlist[0] = CreatePlaylistItem(
- items[0],
- user,
- command.StartPositionTicks ?? 0,
- command.MediaSourceId ?? string.Empty,
- command.AudioStreamIndex,
- command.SubtitleStreamIndex);
-
- for (int i = 1; i < len; i++)
- {
- playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
- }
-
- _logger.LogDebug("{0} - Playlist created", _session.DeviceName);
-
- if (command.PlayCommand == PlayCommand.PlayLast)
- {
- _playlist.AddRange(playlist);
- }
-
- if (command.PlayCommand == PlayCommand.PlayNext)
- {
- _playlist.AddRange(playlist);
- }
-
- if (!command.ControllingUserId.Equals(default))
- {
- _sessionManager.LogSessionActivity(
- _session.Client,
- _session.ApplicationVersion,
- _session.DeviceId,
- _session.DeviceName,
- _session.RemoteEndPoint,
- user);
- }
-
- return PlayItems(playlist, cancellationToken);
- }
-
- private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
- {
- switch (command.Command)
- {
- case PlaystateCommand.Stop:
- _playlist.Clear();
- return _device.SetStop(CancellationToken.None);
-
- case PlaystateCommand.Pause:
- return _device.SetPause(CancellationToken.None);
-
- case PlaystateCommand.Unpause:
- return _device.SetPlay(CancellationToken.None);
-
- case PlaystateCommand.PlayPause:
- return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None);
-
- case PlaystateCommand.Seek:
- return Seek(command.SeekPositionTicks ?? 0);
-
- case PlaystateCommand.NextTrack:
- return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken);
-
- case PlaystateCommand.PreviousTrack:
- return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken);
- }
-
- return Task.CompletedTask;
- }
-
- private async Task Seek(long newPosition)
- {
- var media = _device.CurrentMediaInfo;
-
- if (media is not null)
- {
- var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
-
- if (info.Item is not null && !EnableClientSideSeek(info))
- {
- var user = _session.UserId.Equals(default)
- ? null
- : _userManager.GetUserById(_session.UserId);
- var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
-
- await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
-
- // Send a message to the DLNA device to notify what is the next track in the play list.
- var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
- await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
-
- return;
- }
-
- await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
- }
- }
-
- private bool EnableClientSideSeek(StreamParams info)
- {
- return info.IsDirectStream;
- }
-
- private bool EnableClientSideSeek(StreamInfo info)
- {
- return info.IsDirectStream;
- }
-
- private void AddItemFromId(Guid id, List<BaseItem> list)
- {
- var item = _libraryManager.GetItemById(id);
- if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
- {
- list.Add(item);
- }
- }
-
- private PlaylistItem CreatePlaylistItem(
- BaseItem item,
- User? user,
- long startPostionTicks,
- string? mediaSourceId,
- int? audioStreamIndex,
- int? subtitleStreamIndex)
- {
- var deviceInfo = _device.Properties;
-
- var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
- _dlnaManager.GetDefaultProfile();
-
- var mediaSources = item is IHasMediaSources
- ? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray()
- : Array.Empty<MediaSourceInfo>();
-
- var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
- playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
-
- playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken));
-
- var itemXml = new DidlBuilder(
- profile,
- user,
- _imageProcessor,
- _serverAddress,
- _accessToken,
- _userDataManager,
- _localization,
- _mediaSourceManager,
- _logger,
- _mediaEncoder,
- _libraryManager)
- .GetItemDidl(item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
-
- playlistItem.Didl = itemXml;
-
- return playlistItem;
- }
-
- private string? GetDlnaHeaders(PlaylistItem item)
- {
- var profile = item.Profile;
- var streamInfo = item.StreamInfo;
-
- if (streamInfo.MediaType == DlnaProfileType.Audio)
- {
- return ContentFeatureBuilder.BuildAudioHeader(
- profile,
- streamInfo.Container,
- streamInfo.TargetAudioCodec.FirstOrDefault(),
- streamInfo.TargetAudioBitrate,
- streamInfo.TargetAudioSampleRate,
- streamInfo.TargetAudioChannels,
- streamInfo.TargetAudioBitDepth,
- streamInfo.IsDirectStream,
- streamInfo.RunTimeTicks ?? 0,
- streamInfo.TranscodeSeekInfo);
- }
-
- if (streamInfo.MediaType == DlnaProfileType.Video)
- {
- var list = ContentFeatureBuilder.BuildVideoHeader(
- profile,
- streamInfo.Container,
- streamInfo.TargetVideoCodec.FirstOrDefault(),
- streamInfo.TargetAudioCodec.FirstOrDefault(),
- streamInfo.TargetWidth,
- streamInfo.TargetHeight,
- streamInfo.TargetVideoBitDepth,
- streamInfo.TargetVideoBitrate,
- streamInfo.TargetTimestamp,
- streamInfo.IsDirectStream,
- streamInfo.RunTimeTicks ?? 0,
- streamInfo.TargetVideoProfile,
- streamInfo.TargetVideoRangeType,
- streamInfo.TargetVideoLevel,
- streamInfo.TargetFramerate ?? 0,
- streamInfo.TargetPacketLength,
- streamInfo.TranscodeSeekInfo,
- streamInfo.IsTargetAnamorphic,
- streamInfo.IsTargetInterlaced,
- streamInfo.TargetRefFrames,
- streamInfo.TargetVideoStreamCount,
- streamInfo.TargetAudioStreamCount,
- streamInfo.TargetVideoCodecTag,
- streamInfo.IsTargetAVC);
-
- return list.FirstOrDefault();
- }
-
- return null;
- }
-
- private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
- {
- if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- return new PlaylistItem
- {
- StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
- {
- ItemId = item.Id,
- MediaSources = mediaSources,
- Profile = profile,
- DeviceId = deviceId,
- MaxBitrate = profile.MaxStreamingBitrate,
- MediaSourceId = mediaSourceId,
- AudioStreamIndex = audioStreamIndex,
- SubtitleStreamIndex = subtitleStreamIndex
- }),
-
- Profile = profile
- };
- }
-
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
- {
- return new PlaylistItem
- {
- StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
- {
- ItemId = item.Id,
- MediaSources = mediaSources,
- Profile = profile,
- DeviceId = deviceId,
- MaxBitrate = profile.MaxStreamingBitrate,
- MediaSourceId = mediaSourceId
- }),
-
- Profile = profile
- };
- }
-
- if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
- {
- return PlaylistItemFactory.Create((Photo)item, profile);
- }
-
- throw new ArgumentException("Unrecognized item type.");
- }
-
- /// <summary>
- /// Plays the items.
- /// </summary>
- /// <param name="items">The items.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns><c>true</c> on success.</returns>
- private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default)
- {
- _playlist.Clear();
- _playlist.AddRange(items);
- _logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count);
-
- await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false);
- return true;
- }
-
- private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default)
- {
- if (index < 0 || index >= _playlist.Count)
- {
- _playlist.Clear();
- await _device.SetStop(cancellationToken).ConfigureAwait(false);
- return;
- }
-
- _currentPlaylistIndex = index;
- var currentitem = _playlist[index];
-
- await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
-
- // Send a message to the DLNA device to notify what is the next track in the play list.
- await SendNextTrackMessage(index, cancellationToken).ConfigureAwait(false);
-
- var streamInfo = currentitem.StreamInfo;
- if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
- {
- await SeekAfterTransportChange(streamInfo.StartPositionTicks, CancellationToken.None).ConfigureAwait(false);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and optionally managed resources.
- /// </summary>
- /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _device.Dispose();
- }
-
- _device.PlaybackStart -= OnDevicePlaybackStart;
- _device.PlaybackProgress -= OnDevicePlaybackProgress;
- _device.PlaybackStopped -= OnDevicePlaybackStopped;
- _device.MediaChanged -= OnDeviceMediaChanged;
- _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
- _device.OnDeviceUnavailable = null;
-
- _disposed = true;
- }
-
- private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
- {
- switch (command.Name)
- {
- case GeneralCommandType.VolumeDown:
- return _device.VolumeDown(cancellationToken);
- case GeneralCommandType.VolumeUp:
- return _device.VolumeUp(cancellationToken);
- case GeneralCommandType.Mute:
- return _device.Mute(cancellationToken);
- case GeneralCommandType.Unmute:
- return _device.Unmute(cancellationToken);
- case GeneralCommandType.ToggleMute:
- return _device.ToggleMute(cancellationToken);
- case GeneralCommandType.SetAudioStreamIndex:
- if (command.Arguments.TryGetValue("Index", out string? index))
- {
- if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
- {
- return SetAudioStreamIndex(val);
- }
-
- throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
- }
-
- throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
- case GeneralCommandType.SetSubtitleStreamIndex:
- if (command.Arguments.TryGetValue("Index", out index))
- {
- if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
- {
- return SetSubtitleStreamIndex(val);
- }
-
- throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
- }
-
- throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
- case GeneralCommandType.SetVolume:
- if (command.Arguments.TryGetValue("Volume", out string? vol))
- {
- if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
- {
- return _device.SetVolume(volume, cancellationToken);
- }
-
- throw new ArgumentException("Unsupported volume value supplied.");
- }
-
- throw new ArgumentException("Volume argument cannot be null");
- default:
- return Task.CompletedTask;
- }
- }
-
- private async Task SetAudioStreamIndex(int? newIndex)
- {
- var media = _device.CurrentMediaInfo;
-
- if (media is not null)
- {
- var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
-
- if (info.Item is not null)
- {
- var newPosition = GetProgressPositionTicks(info) ?? 0;
-
- var user = _session.UserId.Equals(default)
- ? null
- : _userManager.GetUserById(_session.UserId);
- var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
-
- await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
-
- // Send a message to the DLNA device to notify what is the next track in the play list.
- var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
- await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
-
- if (EnableClientSideSeek(newItem.StreamInfo))
- {
- await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
- }
- }
- }
- }
-
- private async Task SetSubtitleStreamIndex(int? newIndex)
- {
- var media = _device.CurrentMediaInfo;
-
- if (media is not null)
- {
- var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
-
- if (info.Item is not null)
- {
- var newPosition = GetProgressPositionTicks(info) ?? 0;
-
- var user = _session.UserId.Equals(default)
- ? null
- : _userManager.GetUserById(_session.UserId);
- var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
-
- await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
-
- // Send a message to the DLNA device to notify what is the next track in the play list.
- var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
- await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
-
- if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
- {
- await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
- }
- }
- }
- }
-
- private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
- {
- const int MaxWait = 15000000;
- const int Interval = 500;
-
- var currentWait = 0;
- while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait)
- {
- await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
- currentWait += Interval;
- }
-
- await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
- }
-
- private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
- {
- var value = values.GetValueOrDefault(name);
-
- if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
-
- return null;
- }
-
- private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
- {
- var value = values.GetValueOrDefault(name);
-
- if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
-
- return 0;
- }
-
- /// <inheritdoc />
- public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
-
- return name switch
- {
- SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
- SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
- SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
- _ => Task.CompletedTask // Not supported or needed right now
- };
- }
-
- private class StreamParams
- {
- private MediaSourceInfo? _mediaSource;
- private IMediaSourceManager? _mediaSourceManager;
-
- public Guid ItemId { get; set; }
-
- public bool IsDirectStream { get; set; }
-
- public long StartPositionTicks { get; set; }
-
- public int? AudioStreamIndex { get; set; }
-
- public int? SubtitleStreamIndex { get; set; }
-
- public string? DeviceProfileId { get; set; }
-
- public string? DeviceId { get; set; }
-
- public string? MediaSourceId { get; set; }
-
- public string? LiveStreamId { get; set; }
-
- public BaseItem? Item { get; set; }
-
- public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
- {
- if (_mediaSource is not null)
- {
- return _mediaSource;
- }
-
- if (Item is not IHasMediaSources)
- {
- return null;
- }
-
- if (_mediaSourceManager is not null)
- {
- _mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
- }
-
- return _mediaSource;
- }
-
- private static Guid GetItemId(string url)
- {
- ArgumentException.ThrowIfNullOrEmpty(url);
-
- var parts = url.Split('/');
-
- for (var i = 0; i < parts.Length - 1; i++)
- {
- var part = parts[i];
-
- if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
- || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
- {
- if (Guid.TryParse(parts[i + 1], out var result))
- {
- return result;
- }
- }
- }
-
- return default;
- }
-
- public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
- {
- ArgumentException.ThrowIfNullOrEmpty(url);
-
- var request = new StreamParams
- {
- ItemId = GetItemId(url)
- };
-
- if (request.ItemId.Equals(default))
- {
- return request;
- }
-
- var index = url.IndexOf('?', StringComparison.Ordinal);
- if (index == -1)
- {
- return request;
- }
-
- var query = url.Substring(index + 1);
- Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
-
- request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
- 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);
- request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
- request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
- request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
-
- request.Item = libraryManager.GetItemById(request.ItemId);
-
- request._mediaSourceManager = mediaSourceManager;
-
- return request;
- }
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
deleted file mode 100644
index ef617422c..000000000
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ /dev/null
@@ -1,258 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Events;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.PlayTo
-{
- public sealed class PlayToManager : IDisposable
- {
- private readonly ILogger _logger;
- private readonly ISessionManager _sessionManager;
-
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
- private readonly IServerApplicationHost _appHost;
- private readonly IImageProcessor _imageProcessor;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly IUserDataManager _userDataManager;
- private readonly ILocalizationManager _localization;
-
- private readonly IDeviceDiscovery _deviceDiscovery;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IMediaEncoder _mediaEncoder;
-
- private bool _disposed;
- private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
- private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
-
- public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
- {
- _logger = logger;
- _sessionManager = sessionManager;
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dlnaManager = dlnaManager;
- _appHost = appHost;
- _imageProcessor = imageProcessor;
- _deviceDiscovery = deviceDiscovery;
- _httpClientFactory = httpClientFactory;
- _userDataManager = userDataManager;
- _localization = localization;
- _mediaSourceManager = mediaSourceManager;
- _mediaEncoder = mediaEncoder;
- }
-
- public void Start()
- {
- _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
- }
-
- private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
- {
- if (_disposed)
- {
- return;
- }
-
- var info = e.Argument;
-
- if (!info.Headers.TryGetValue("USN", out string? usn))
- {
- usn = string.Empty;
- }
-
- if (!info.Headers.TryGetValue("NT", out string? nt))
- {
- nt = string.Empty;
- }
-
- // It has to report that it's a media renderer
- if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)
- && !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- var cancellationToken = _disposeCancellationTokenSource.Token;
-
- await _sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
- {
- if (_disposed)
- {
- return;
- }
-
- if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1))
- {
- return;
- }
-
- await AddDevice(info, cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating PlayTo device.");
- }
- finally
- {
- _sessionLock.Release();
- }
- }
-
- internal static string GetUuid(string usn)
- {
- const string UuidStr = "uuid:";
- const string UuidColonStr = "::";
-
- var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
- if (index == -1)
- {
- return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
-
- ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..];
-
- index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- tmp = tmp[..index];
- }
-
- index = tmp.IndexOf('{');
- if (index != -1)
- {
- int endIndex = tmp.IndexOf('}');
- if (endIndex != -1)
- {
- tmp = tmp[(index + 1)..endIndex];
- }
- }
-
- return tmp.ToString();
- }
-
- private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken)
- {
- var uri = info.Location;
- _logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
-
- if (info.Headers.TryGetValue("USN", out string? uuid))
- {
- uuid = GetUuid(uuid);
- }
- else
- {
- uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
-
- var sessionInfo = await _sessionManager
- .LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
- .ConfigureAwait(false);
-
- var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
-
- if (controller is null)
- {
- var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
- if (device is null)
- {
- _logger.LogError("Ignoring device as xml response is invalid.");
- return;
- }
-
- string deviceName = device.Properties.Name;
-
- _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
-
- string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress);
-
- controller = new PlayToController(
- sessionInfo,
- _sessionManager,
- _libraryManager,
- _logger,
- _dlnaManager,
- _userManager,
- _imageProcessor,
- serverAddress,
- null,
- _deviceDiscovery,
- _userDataManager,
- _localization,
- _mediaSourceManager,
- _mediaEncoder,
- device);
-
- sessionInfo.AddController(controller);
-
- var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
- _dlnaManager.GetDefaultProfile();
-
- _sessionManager.ReportCapabilities(sessionInfo.Id, new ClientCapabilities
- {
- PlayableMediaTypes = profile.GetSupportedMediaTypes(),
-
- SupportedCommands = new[]
- {
- GeneralCommandType.VolumeDown,
- GeneralCommandType.VolumeUp,
- GeneralCommandType.Mute,
- GeneralCommandType.Unmute,
- GeneralCommandType.ToggleMute,
- GeneralCommandType.SetVolume,
- GeneralCommandType.SetAudioStreamIndex,
- GeneralCommandType.SetSubtitleStreamIndex,
- GeneralCommandType.PlayMediaSource
- },
-
- SupportsMediaControl = true
- });
-
- _logger.LogInformation("DLNA Session created for {0} - {1}", device.Properties.Name, device.Properties.ModelName);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
-
- try
- {
- _disposeCancellationTokenSource.Cancel();
- }
- catch (Exception ex)
- {
- _logger.LogDebug(ex, "Error while disposing PlayToManager");
- }
-
- _sessionLock.Dispose();
- _disposeCancellationTokenSource.Dispose();
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
deleted file mode 100644
index c95d8b1e8..000000000
--- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Dlna.PlayTo
-{
- public class PlaybackProgressEventArgs : EventArgs
- {
- public PlaybackProgressEventArgs(UBaseObject mediaInfo)
- {
- MediaInfo = mediaInfo;
- }
-
- public UBaseObject MediaInfo { get; set; }
- }
-}
diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
deleted file mode 100644
index 619c861ed..000000000
--- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Dlna.PlayTo
-{
- public class PlaybackStartEventArgs : EventArgs
- {
- public PlaybackStartEventArgs(UBaseObject mediaInfo)
- {
- MediaInfo = mediaInfo;
- }
-
- public UBaseObject MediaInfo { get; set; }
- }
-}
diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
deleted file mode 100644
index d0ec25059..000000000
--- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace Emby.Dlna.PlayTo
-{
- public class PlaybackStoppedEventArgs : EventArgs
- {
- public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
- {
- MediaInfo = mediaInfo;
- }
-
- public UBaseObject MediaInfo { get; set; }
- }
-}
diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs
deleted file mode 100644
index 5056e69ae..000000000
--- a/Emby.Dlna/PlayTo/PlaylistItem.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using MediaBrowser.Model.Dlna;
-
-namespace Emby.Dlna.PlayTo
-{
- public class PlaylistItem
- {
- public string StreamUrl { get; set; }
-
- public string Didl { get; set; }
-
- public StreamInfo StreamInfo { get; set; }
-
- public DeviceProfile Profile { get; set; }
- }
-}
diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
deleted file mode 100644
index 53cd05cfd..000000000
--- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.IO;
-using System.Linq;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Session;
-
-namespace Emby.Dlna.PlayTo
-{
- public static class PlaylistItemFactory
- {
- public static PlaylistItem Create(Photo item, DeviceProfile profile)
- {
- var playlistItem = new PlaylistItem
- {
- StreamInfo = new StreamInfo
- {
- ItemId = item.Id,
- MediaType = DlnaProfileType.Photo,
- DeviceProfile = profile
- },
-
- Profile = profile
- };
-
- var directPlay = profile.DirectPlayProfiles
- .FirstOrDefault(i => i.Type == DlnaProfileType.Photo && IsSupported(i, item));
-
- if (directPlay is not null)
- {
- playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream;
- playlistItem.StreamInfo.Container = Path.GetExtension(item.Path);
-
- return playlistItem;
- }
-
- var transcodingProfile = profile.TranscodingProfiles
- .FirstOrDefault(i => i.Type == DlnaProfileType.Photo);
-
- if (transcodingProfile is not null)
- {
- playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode;
- playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.');
- }
-
- return playlistItem;
- }
-
- private static bool IsSupported(DirectPlayProfile profile, Photo item)
- {
- var mediaPath = item.Path;
-
- if (profile.Container.Length > 0)
- {
- // Check container type
- var mediaContainer = (Path.GetExtension(mediaPath) ?? string.Empty).TrimStart('.');
-
- if (!profile.SupportsContainer(mediaContainer))
- {
- return false;
- }
- }
-
- return true;
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
deleted file mode 100644
index 6b2096d9d..000000000
--- a/Emby.Dlna/PlayTo/TransportCommands.cs
+++ /dev/null
@@ -1,181 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Xml.Linq;
-using Emby.Dlna.Common;
-using Emby.Dlna.Ssdp;
-
-namespace Emby.Dlna.PlayTo
-{
- public class TransportCommands
- {
- private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
-
- public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
-
- public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
-
- public static TransportCommands Create(XDocument document)
- {
- var command = new TransportCommands();
-
- var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList");
-
- foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action"))
- {
- command.ServiceActions.Add(ServiceActionFromXml(container));
- }
-
- var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
-
- if (stateValues is not null)
- {
- foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
- {
- command.StateVariables.Add(FromXml(container));
- }
- }
-
- return command;
- }
-
- private static ServiceAction ServiceActionFromXml(XElement container)
- {
- var serviceAction = new ServiceAction
- {
- Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
- };
-
- var argumentList = serviceAction.ArgumentList;
-
- foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
- {
- argumentList.Add(ArgumentFromXml(arg));
- }
-
- return serviceAction;
- }
-
- private static Argument ArgumentFromXml(XElement container)
- {
- ArgumentNullException.ThrowIfNull(container);
-
- return new Argument
- {
- Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
- Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
- RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
- };
- }
-
- private static StateVariable FromXml(XElement container)
- {
- var allowedValues = Array.Empty<string>();
- var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
- .FirstOrDefault();
-
- if (element is not null)
- {
- var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
-
- allowedValues = values.Select(child => child.Value).ToArray();
- }
-
- return new StateVariable
- {
- Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
- DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
- AllowedValues = allowedValues
- };
- }
-
- public string BuildPost(ServiceAction action, string xmlNamespace)
- {
- var stateString = string.Empty;
-
- foreach (var arg in action.ArgumentList)
- {
- if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
- {
- continue;
- }
-
- if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
- {
- stateString += BuildArgumentXml(arg, "0");
- }
- else
- {
- stateString += BuildArgumentXml(arg, null);
- }
- }
-
- return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
- }
-
- public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
- {
- var stateString = string.Empty;
-
- foreach (var arg in action.ArgumentList)
- {
- if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
- {
- continue;
- }
-
- if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
- {
- stateString += BuildArgumentXml(arg, "0");
- }
- else
- {
- stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
- }
- }
-
- return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
- }
-
- public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary)
- {
- var stateString = string.Empty;
-
- foreach (var arg in action.ArgumentList)
- {
- if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
- {
- stateString += BuildArgumentXml(arg, "0");
- }
- else if (dictionary.TryGetValue(arg.Name, out var argValue))
- {
- stateString += BuildArgumentXml(arg, argValue);
- }
- else
- {
- stateString += BuildArgumentXml(arg, value.ToString());
- }
- }
-
- return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
- }
-
- private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
- {
- var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
-
- if (state is not null)
- {
- var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
- (state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
-
- return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue);
- }
-
- return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs
deleted file mode 100644
index 0d6a78438..000000000
--- a/Emby.Dlna/PlayTo/TransportState.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Dlna.PlayTo
-{
- /// <summary>
- /// Core of the AVTransport service. It defines the conceptually top-
- /// level state of the transport, for example, whether it is playing, recording, etc.
- /// </summary>
- public enum TransportState
- {
- STOPPED,
- PLAYING,
- TRANSITIONING,
- PAUSED_PLAYBACK
- }
-}
diff --git a/Emby.Dlna/PlayTo/UpnpContainer.cs b/Emby.Dlna/PlayTo/UpnpContainer.cs
deleted file mode 100644
index 017d51e60..000000000
--- a/Emby.Dlna/PlayTo/UpnpContainer.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Xml.Linq;
-using Emby.Dlna.Ssdp;
-
-namespace Emby.Dlna.PlayTo
-{
- public class UpnpContainer : UBaseObject
- {
- public static UBaseObject Create(XElement container)
- {
- ArgumentNullException.ThrowIfNull(container);
-
- return new UBaseObject
- {
- Id = container.GetAttributeValue(UPnpNamespaces.Id),
- ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
- Title = container.GetValue(UPnpNamespaces.Title),
- IconUrl = container.GetValue(UPnpNamespaces.Artwork),
- UpnpClass = container.GetValue(UPnpNamespaces.Class)
- };
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs
deleted file mode 100644
index 2e0f2063b..000000000
--- a/Emby.Dlna/PlayTo/uBaseObject.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-
-namespace Emby.Dlna.PlayTo
-{
- public class UBaseObject
- {
- public string Id { get; set; }
-
- public string ParentId { get; set; }
-
- public string Title { get; set; }
-
- public string SecondText { get; set; }
-
- public string IconUrl { get; set; }
-
- public string MetaData { get; set; }
-
- public string Url { get; set; }
-
- public IReadOnlyList<string> ProtocolInfo { get; set; }
-
- public string UpnpClass { get; set; }
-
- public string MediaType
- {
- get
- {
- var classType = UpnpClass ?? string.Empty;
-
- if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1)
- {
- return MediaBrowser.Model.Entities.MediaType.Audio;
- }
-
- if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
- {
- return MediaBrowser.Model.Entities.MediaType.Video;
- }
-
- if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
- {
- return MediaBrowser.Model.Entities.MediaType.Photo;
- }
-
- return null;
- }
- }
-
- public bool Equals(UBaseObject obj)
- {
- ArgumentNullException.ThrowIfNull(obj);
-
- return string.Equals(Id, obj.Id, StringComparison.Ordinal);
- }
- }
-}
diff --git a/Emby.Dlna/PlayTo/uPnpNamespaces.cs b/Emby.Dlna/PlayTo/uPnpNamespaces.cs
deleted file mode 100644
index 5042d4493..000000000
--- a/Emby.Dlna/PlayTo/uPnpNamespaces.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Xml.Linq;
-
-namespace Emby.Dlna.PlayTo
-{
- public static class UPnpNamespaces
- {
- public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
-
- public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
-
- public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
-
- public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0";
-
- public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/";
-
- public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1";
-
- public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1";
-
- public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1";
-
- public static XName Containers { get; } = Ns + "container";
-
- public static XName Items { get; } = Ns + "item";
-
- public static XName Title { get; } = Dc + "title";
-
- public static XName Creator { get; } = Dc + "creator";
-
- public static XName Artist { get; } = UPnp + "artist";
-
- public static XName Id { get; } = "id";
-
- public static XName ParentId { get; } = "parentID";
-
- public static XName Class { get; } = UPnp + "class";
-
- public static XName Artwork { get; } = UPnp + "albumArtURI";
-
- public static XName Description { get; } = Dc + "description";
-
- public static XName LongDescription { get; } = UPnp + "longDescription";
-
- public static XName Album { get; } = UPnp + "album";
-
- public static XName Author { get; } = UPnp + "author";
-
- public static XName Director { get; } = UPnp + "director";
-
- public static XName PlayCount { get; } = UPnp + "playbackCount";
-
- public static XName Tracknumber { get; } = UPnp + "originalTrackNumber";
-
- public static XName Res { get; } = Ns + "res";
-
- public static XName Duration { get; } = "duration";
-
- public static XName ProtocolInfo { get; } = "protocolInfo";
-
- public static XName ServiceStateTable { get; } = Svc + "serviceStateTable";
-
- public static XName StateVariable { get; } = Svc + "stateVariable";
- }
-}
diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs
deleted file mode 100644
index 54a0a87a8..000000000
--- a/Emby.Dlna/Profiles/DefaultProfile.cs
+++ /dev/null
@@ -1,179 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using MediaBrowser.Model.Dlna;
-
-namespace Emby.Dlna.Profiles
-{
- [System.Xml.Serialization.XmlRoot("Profile")]
- public class DefaultProfile : DeviceProfile
- {
- public DefaultProfile()
- {
- Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- Name = "Generic Device";
-
- ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
-
- Manufacturer = "Jellyfin";
- ModelDescription = "UPnP/AV 1.0 Compliant Media Server";
- ModelName = "Jellyfin Server";
- ModelNumber = "01";
- ModelUrl = "https://github.com/jellyfin/jellyfin";
- ManufacturerUrl = "https://github.com/jellyfin/jellyfin";
-
- AlbumArtPn = "JPEG_SM";
-
- MaxAlbumArtHeight = 480;
- MaxAlbumArtWidth = 480;
-
- MaxIconWidth = 48;
- MaxIconHeight = 48;
-
- MaxStreamingBitrate = 140000000;
- MaxStaticBitrate = 140000000;
- MusicStreamingTranscodingBitrate = 192000;
-
- EnableAlbumArtInDidl = false;
-
- TranscodingProfiles = new[]
- {
- new TranscodingProfile
- {
- Container = "mp3",
- AudioCodec = "mp3",
- Type = DlnaProfileType.Audio
- },
-
- new TranscodingProfile
- {
- Container = "ts",
- Type = DlnaProfileType.Video,
- AudioCodec = "aac",
- VideoCodec = "h264"
- },
-
- new TranscodingProfile
- {
- Container = "jpeg",
- Type = DlnaProfileType.Photo
- }
- };
-
- DirectPlayProfiles = new[]
- {
- new DirectPlayProfile
- {
- // play all
- Container = string.Empty,
- Type = DlnaProfileType.Video
- },
-
- new DirectPlayProfile
- {
- // play all
- Container = string.Empty,
- Type = DlnaProfileType.Audio
- }
- };
-
- SubtitleProfiles = new[]
- {
- new SubtitleProfile
- {
- Format = "srt",
- Method = SubtitleDeliveryMethod.External,
- },
-
- new SubtitleProfile
- {
- Format = "sub",
- Method = SubtitleDeliveryMethod.External,
- },
-
- new SubtitleProfile
- {
- Format = "sup",
- Method = SubtitleDeliveryMethod.External
- },
-
- new SubtitleProfile
- {
- Format = "srt",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "ass",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "ssa",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "smi",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "dvdsub",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "pgs",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "pgssub",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "sub",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "sup",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "subrip",
- Method = SubtitleDeliveryMethod.Embed
- },
-
- new SubtitleProfile
- {
- Format = "vtt",
- Method = SubtitleDeliveryMethod.Embed
- }
- };
-
- ResponseProfiles = new[]
- {
- new ResponseProfile
- {
- Container = "m4v",
- Type = DlnaProfileType.Video,
- MimeType = "video/mp4"
- }
- };
- }
- }
-}
diff --git a/Emby.Dlna/Profiles/Xml/Default.xml b/Emby.Dlna/Profiles/Xml/Default.xml
deleted file mode 100644
index 9460f9d5a..000000000
--- a/Emby.Dlna/Profiles/Xml/Default.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Generic Device</Name>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="" type="Video" />
- <DirectPlayProfile container="" type="Audio" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles />
- <ResponseProfiles>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="External" />
- <SubtitleProfile format="sub" method="External" />
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="ass" method="Embed" />
- <SubtitleProfile format="ssa" method="Embed" />
- <SubtitleProfile format="smi" method="Embed" />
- <SubtitleProfile format="dvdsub" method="Embed" />
- <SubtitleProfile format="pgs" method="Embed" />
- <SubtitleProfile format="pgssub" method="Embed" />
- <SubtitleProfile format="sub" method="Embed" />
- <SubtitleProfile format="subrip" method="Embed" />
- <SubtitleProfile format="vtt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Denon AVR.xml b/Emby.Dlna/Profiles/Xml/Denon AVR.xml
deleted file mode 100644
index 571786906..000000000
--- a/Emby.Dlna/Profiles/Xml/Denon AVR.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Denon AVR</Name>
- <Identification>
- <FriendlyName>Denon:\[AVR:.*</FriendlyName>
- <Manufacturer>Denon</Manufacturer>
- <Headers />
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles>
- <CodecProfile type="Audio" container="flac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioSampleRate" value="96000" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="External" />
- <SubtitleProfile format="sub" method="External" />
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="ass" method="Embed" />
- <SubtitleProfile format="ssa" method="Embed" />
- <SubtitleProfile format="smi" method="Embed" />
- <SubtitleProfile format="dvdsub" method="Embed" />
- <SubtitleProfile format="pgs" method="Embed" />
- <SubtitleProfile format="pgssub" method="Embed" />
- <SubtitleProfile format="sub" method="Embed" />
- <SubtitleProfile format="subrip" method="Embed" />
- <SubtitleProfile format="vtt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml b/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml
deleted file mode 100644
index eea0febfd..000000000
--- a/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>DirecTV HD-DVR</Name>
- <Identification>
- <FriendlyName>^DIRECTV.*$</FriendlyName>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="DIRECTV" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>true</RequiresPlainVideoItems>
- <RequiresPlainFolders>true</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg2video" type="Video" />
- <DirectPlayProfile container="jpeg,jpg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mpeg" type="Video" videoCodec="mpeg2video" audioCodec="mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles>
- <CodecProfile type="Video" codec="mpeg2video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="8192000" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Audio" codec="mp2">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml b/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml
deleted file mode 100644
index 5b299577e..000000000
--- a/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml
+++ /dev/null
@@ -1,96 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Dish Hopper-Joey</Name>
- <Identification>
- <Manufacturer>Echostar Technologies LLC</Manufacturer>
- <ManufacturerUrl>http://www.echostar.com</ManufacturerUrl>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="Zip_" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mp2t:http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="mp4,mkv,mpeg,ts" audioCodec="mp3,ac3,aac,he-aac,pcm" videoCodec="h264,mpeg2video" type="Video" />
- <DirectPlayProfile container="mp3,alac,flac" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3,he-aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio">
- <Conditions>
- <ProfileCondition condition="Equals" property="IsSecondaryAudio" value="false" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="mkv,ts,mpegts" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml
deleted file mode 100644
index 20f5ba79b..000000000
--- a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml
+++ /dev/null
@@ -1,92 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>LG Smart TV</Name>
- <Identification>
- <FriendlyName>LG.*</FriendlyName>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="LG" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts,avi,mkv,m2ts" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="mp3" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="mpeg4">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3,eac3,aac,mp3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" type="Video" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="srt" method="External" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml b/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml
deleted file mode 100644
index d01e3a145..000000000
--- a/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Linksys DMA2100</Name>
- <Identification>
- <ModelName>DMA2100us</ModelName>
- <Headers />
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />
- <DirectPlayProfile container="avi,mp4,mkv,ts,mpegts,m4v" type="Video" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles />
- <ResponseProfiles>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Marantz.xml b/Emby.Dlna/Profiles/Xml/Marantz.xml
deleted file mode 100644
index 0cc9c86e8..000000000
--- a/Emby.Dlna/Profiles/Xml/Marantz.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Marantz</Name>
- <Identification>
- <Manufacturer>Marantz</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="Marantz" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="aac,mp3,wav,wma,flac" type="Audio" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles />
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="External" />
- <SubtitleProfile format="sub" method="External" />
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="ass" method="Embed" />
- <SubtitleProfile format="ssa" method="Embed" />
- <SubtitleProfile format="smi" method="Embed" />
- <SubtitleProfile format="dvdsub" method="Embed" />
- <SubtitleProfile format="pgs" method="Embed" />
- <SubtitleProfile format="pgssub" method="Embed" />
- <SubtitleProfile format="sub" method="Embed" />
- <SubtitleProfile format="subrip" method="Embed" />
- <SubtitleProfile format="vtt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/MediaMonkey.xml b/Emby.Dlna/Profiles/Xml/MediaMonkey.xml
deleted file mode 100644
index 9d5ddc3d1..000000000
--- a/Emby.Dlna/Profiles/Xml/MediaMonkey.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>MediaMonkey</Name>
- <Identification>
- <FriendlyName>MediaMonkey</FriendlyName>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="MediaMonkey" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="aac,mp3,mpa,wav,wma,mp2,ogg,oga,webma,ape,opus,flac,m4a" type="Audio" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles />
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="External" />
- <SubtitleProfile format="sub" method="External" />
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="ass" method="Embed" />
- <SubtitleProfile format="ssa" method="Embed" />
- <SubtitleProfile format="smi" method="Embed" />
- <SubtitleProfile format="dvdsub" method="Embed" />
- <SubtitleProfile format="pgs" method="Embed" />
- <SubtitleProfile format="pgssub" method="Embed" />
- <SubtitleProfile format="sub" method="Embed" />
- <SubtitleProfile format="subrip" method="Embed" />
- <SubtitleProfile format="vtt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml b/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml
deleted file mode 100644
index 8f766853b..000000000
--- a/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Panasonic Viera</Name>
- <Identification>
- <FriendlyName>VIERA</FriendlyName>
- <Manufacturer>Panasonic</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="Panasonic MIL DLNA" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>10</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:pv" value="http://www.pv.com/pvns/" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,pcm_dvd" videoCodec="mpeg2video,mpeg4" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="aac,ac3,dca,mp3,mp2,pcm,dts" videoCodec="h264,mpeg2video" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="aac,mp3,mp2" videoCodec="h264,mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,mp3,pcm" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="mov" audioCodec="aac,pcm" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="flv" audioCodec="aac" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
- <DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="ts,mpegts" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="srt" method="External" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml b/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml
deleted file mode 100644
index aa881d014..000000000
--- a/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml
+++ /dev/null
@@ -1,92 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Popcorn Hour</Name>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="mp4,mov,m4v" audioCodec="aac" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,eac3,mp3,mp2,pcm" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="asf,wmv" audioCodec="wmav2,wmapro" videoCodec="wmv3,vc1" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="mp3,ac3,eac3,mp2,pcm" videoCodec="mpeg4,msmpeg4" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="aac,mp3,ac3,eac3,mp2,pcm" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="aac,mp3,flac,ogg,wma,wav" type="Audio" />
- <DirectPlayProfile container="jpeg,gif,bmp,png" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="EqualsAny" property="VideoProfile" value="baseline|constrained baseline" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Audio" codec="aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Audio" codec="mp3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="320000" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml b/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml
deleted file mode 100644
index 7160a9c2e..000000000
--- a/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Samsung Smart TV</Name>
- <Identification>
- <ModelUrl>samsung.com</ModelUrl>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="SEC_" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:sec" value="http://www.sec.co.kr/" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="asf" audioCodec="mp3,ac3,wmav2,wmapro,wmavoice" videoCodec="h264,mpeg4,mjpeg" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="mp3,ac3,dca,dts" videoCodec="h264,mpeg4,mjpeg" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="mp3,ac3,dca,aac,dts" videoCodec="h264,mpeg4,mjpeg4" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="mp3,aac" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="3gp" audioCodec="aac,he-aac" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="mpg,mpeg" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
- <DirectPlayProfile container="vro,vob" audioCodec="ac3,mp2,mp3" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="ts" audioCodec="ac3,aac,mp3,eac3" videoCodec="mpeg2video,h264,vc1" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmavoice" videoCodec="wmv2,wmv3" type="Video" />
- <DirectPlayProfile container="mp3,flac" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="mpeg2video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="30720000" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video" codec="mpeg4">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="8192000" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="37500000" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video" codec="wmv2,wmv3,vc1">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="25600000" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="wmav2,dca,aac,mp3,dts">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="avi" type="Video" mimeType="video/x-msvideo">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mkv" type="Video" mimeType="video/x-mkv">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="flac" type="Audio" mimeType="audio/x-flac">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="srt" method="External" didlMode="CaptionInfoEx" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml b/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml
deleted file mode 100644
index c9b907e58..000000000
--- a/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sharp Smart TV</Name>
- <Identification>
- <Manufacturer>Sharp</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="Sharp" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>true</RequiresPlainVideoItems>
- <RequiresPlainFolders>true</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="m4v,mkv,avi,mov,mp4" audioCodec="aac,mp3,ac3,dts,dca" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="asf,wmv" type="Video" />
- <DirectPlayProfile container="mpg,mpeg" audioCodec="mp3,aac" videoCodec="mpeg2video" type="Video" />
- <DirectPlayProfile container="flv" audioCodec="mp3,aac" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="mp3,wav" type="Audio" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3,dts,dca" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles />
- <ResponseProfiles>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="srt" method="External" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml
deleted file mode 100644
index 2c5614883..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Blu-ray Player 2013</Name>
- <Identification>
- <ModelNumber>BDP-2013</ModelNumber>
- <Headers>
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S1100" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S3100" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S5100" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S6100" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S7100" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
- <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,mp2,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,pcm,mp3" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="ac3,dca,aac,mp3,pcm,dts" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="m2ts,mts" audioCodec="aac,mp3,ac3,dca,dts" videoCodec="h264,mpeg4,vc1" type="Video" />
- <DirectPlayProfile container="wmv,asf" type="Video" />
- <DirectPlayProfile container="mp3,m4a,wma,wav" type="Audio" />
- <DirectPlayProfile container="jpeg,png,gif" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml
deleted file mode 100644
index 44f9821b3..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Blu-ray Player 2014</Name>
- <Identification>
- <ModelNumber>BDP-2014</ModelNumber>
- <Headers>
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S1200" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S3200" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S5200" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S6200" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S7200" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
- <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,mp2,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,pcm,mp3" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="ac3,dca,aac,mp3,pcm,dts" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="m2ts,mts" audioCodec="aac,mp3,ac3,dca,dts" videoCodec="h264,mpeg4,vc1" type="Video" />
- <DirectPlayProfile container="wmv,asf" type="Video" />
- <DirectPlayProfile container="mp3,m4a,wma,wav" type="Audio" />
- <DirectPlayProfile container="jpeg,png,gif" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml
deleted file mode 100644
index a7d17c1a0..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Blu-ray Player 2015</Name>
- <Identification>
- <ModelNumber>BDP-2015</ModelNumber>
- <Headers>
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S1500" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S3500" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S6500" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
- <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,mp2,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,pcm,mp3" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="ac3,dca,aac,mp3,pcm,dts" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="m2ts,mts" audioCodec="aac,mp3,ac3,dca,dts" videoCodec="h264,mpeg4,vc1" type="Video" />
- <DirectPlayProfile container="wmv,asf" type="Video" />
- <DirectPlayProfile container="mp3,m4a,wma,wav" type="Audio" />
- <DirectPlayProfile container="jpeg,png,gif" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml
deleted file mode 100644
index b42b1e84f..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Blu-ray Player 2016</Name>
- <Identification>
- <ModelNumber>BDP-2016</ModelNumber>
- <Headers>
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S1700" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S3700" match="Substring" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="BDP-S6700" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
- <DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,mp2,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,pcm,mp3" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="ac3,dca,aac,mp3,pcm,dts" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="m2ts,mts" audioCodec="aac,mp3,ac3,dca,dts" videoCodec="h264,mpeg4,vc1" type="Video" />
- <DirectPlayProfile container="wmv,asf" type="Video" />
- <DirectPlayProfile container="mp3,m4a,wma,wav" type="Audio" />
- <DirectPlayProfile container="jpeg,png,gif" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml
deleted file mode 100644
index 46857caf0..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml
+++ /dev/null
@@ -1,115 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Blu-ray Player</Name>
- <Identification>
- <FriendlyName>Blu-ray Disc Player</FriendlyName>
- <Manufacturer>Sony</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="X-AV-Client-Info" value="(Blu-ray Disc Player|Home Theater System|Home Theatre System|Media Player)" match="Regex" />
- <HttpHeaderInfo name="X-AV-Physical-Unit-Info" value="(Blu-ray Disc Player|Home Theater System|Home Theatre System|Media Player)" match="Regex" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/divx:DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMAFULL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=44100;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=1:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/L16;rate=48000;channels=2:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/mp4:DLNA.ORG_PN=AAC_ISO_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/vnd.dlna.adts:DLNA.ORG_PN=AAC_ADTS_320;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/flac:DLNA.ORG_PN=FLAC;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:audio/ogg:DLNA.ORG_PN=OGG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/png:DLNA.ORG_PN=PNG_TN;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_NA_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_NA_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_KO_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_KO_ISO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_JP_T;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-msvideo:DLNA.ORG_PN=AVI;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-flv:DLNA.ORG_PN=FLV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-dvr:DLNA.ORG_PN=DVR_MS;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/wtv:DLNA.ORG_PN=WTV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/ogg:DLNA.ORG_PN=OGV;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/vnd.rn-realvideo:DLNA.ORG_PN=REAL_VIDEO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_FULL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L1_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L2_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/x-ms-asf:DLNA.ORG_PN=VC1_ASF_AP_L3_WMA;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_P2_3GPP_SP_L0B_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_3GPP_P0_L10_AMR;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:video/3gpp:DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="ac3,mp3,pcm" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="avi,mp4,m4v" audioCodec="ac3,aac,mp3,pcm" videoCodec="mpeg4,h264" type="Video" />
- <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="mpeg2video" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg4,vc1" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="avi" type="Video" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mkv" type="Video" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" type="Video" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mp4" type="Video" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mpeg" type="Video" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mp3" type="Audio" mimeType="audio/mpeg">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml
deleted file mode 100644
index 1461db311..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml
+++ /dev/null
@@ -1,133 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Bravia (2010)</Name>
- <Identification>
- <FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName>
- <Manufacturer>Sony</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>http://www.microsoft.com/</ModelUrl>
- <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_TN</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <SonyAggregationFlags>10</SonyAggregationFlags>
- <ProtocolInfo>http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
- <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video" codec="mpeg2video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- <ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="mp3,mp2">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
- <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml
deleted file mode 100644
index 7c5f2b181..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml
+++ /dev/null
@@ -1,139 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Bravia (2011)</Name>
- <Identification>
- <FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName>
- <Manufacturer>Sony</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>http://www.microsoft.com/</ModelUrl>
- <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_TN</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <SonyAggregationFlags>10</SonyAggregationFlags>
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="mp3" videoCodec="mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="mp3" videoCodec="mpeg2video,mpeg1video" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
- <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video" codec="mpeg2video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- <ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="mp3,mp2">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
- <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml
deleted file mode 100644
index 842a8fba3..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml
+++ /dev/null
@@ -1,115 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Bravia (2012)</Name>
- <Identification>
- <FriendlyName>KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*</FriendlyName>
- <Manufacturer>Sony</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*" match="Regex" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>http://www.microsoft.com/</ModelUrl>
- <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_TN</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <SonyAggregationFlags>10</SonyAggregationFlags>
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
- <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="mp3,mp2">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
- <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml
deleted file mode 100644
index f1135c3fe..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml
+++ /dev/null
@@ -1,114 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Bravia (2013)</Name>
- <Identification>
- <FriendlyName>KDL-[0-9]{2}[WR][5689][0-9]{2}A.*</FriendlyName>
- <Manufacturer>Sony</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*" match="Regex" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>http://www.microsoft.com/</ModelUrl>
- <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_TN</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <SonyAggregationFlags>10</SonyAggregationFlags>
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,eac3,aac,mp3" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="mov" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4,mjpeg" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="ac3,eac3,aac,mp3,mp2,pcm,vorbis" videoCodec="h264,mpeg4,vp8" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="ac3,eac3,mp3" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mjpeg" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
- <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
- <DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" />
- <DirectPlayProfile container="wav" audioCodec="pcm" type="Audio" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="mp3,mp2">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
- <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml
deleted file mode 100644
index 85c7868c6..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml
+++ /dev/null
@@ -1,114 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony Bravia (2014)</Name>
- <Identification>
- <FriendlyName>(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*</FriendlyName>
- <Manufacturer>Sony</Manufacturer>
- <Headers>
- <HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*" match="Regex" />
- </Headers>
- </Identification>
- <Manufacturer>Microsoft Corporation</Manufacturer>
- <ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
- <ModelName>Windows Media Player Sharing</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>3.0</ModelNumber>
- <ModelUrl>http://www.microsoft.com/</ModelUrl>
- <EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_TN</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <SonyAggregationFlags>10</SonyAggregationFlags>
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes>
- <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
- </XmlRootAttributes>
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,eac3,aac,mp3" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,m4v" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="mov" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4,mjpeg" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="ac3,eac3,aac,mp3,mp2,pcm,vorbis" videoCodec="h264,mpeg4,vp8" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="ac3,eac3,mp3" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mjpeg" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
- <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
- <DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" />
- <DirectPlayProfile container="wav" audioCodec="pcm" type="Audio" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="mp3,mp2">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
- <ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
- <Conditions>
- <ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
- </Conditions>
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
deleted file mode 100644
index 129b188e2..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony PlayStation 3</Name>
- <Identification>
- <FriendlyName>PLAYSTATION 3</FriendlyName>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="PLAYSTATION 3" match="Substring" />
- <HttpHeaderInfo name="X-AV-Client-Info" value="PLAYSTATION 3" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_TN</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <SonyAggregationFlags>10</SonyAggregationFlags>
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="aac,mp3,wav" type="Audio" />
- <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="640000" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="wmapro">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="mp4,mov" audioCodec="aac" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="avi" type="Video" orgPn="AVI" mimeType="video/divx">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="wav" type="Audio" mimeType="audio/wav">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
deleted file mode 100644
index 592119305..000000000
--- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml
+++ /dev/null
@@ -1,108 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Sony PlayStation 4</Name>
- <Identification>
- <FriendlyName>PLAYSTATION 4</FriendlyName>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="PLAYSTATION 4" match="Substring" />
- <HttpHeaderInfo name="X-AV-Client-Info" value="PLAYSTATION 4" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_TN</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <SonyAggregationFlags>10</SonyAggregationFlags>
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="aac,mp3,wav" type="Audio" />
- <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="640000" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="wmapro">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="mp4,mov" audioCodec="aac" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="avi" type="Video" orgPn="AVI" mimeType="video/divx">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="wav" type="Audio" mimeType="audio/wav">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/WDTV Live.xml b/Emby.Dlna/Profiles/Xml/WDTV Live.xml
deleted file mode 100644
index ccb74ee64..000000000
--- a/Emby.Dlna/Profiles/Xml/WDTV Live.xml
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>WDTV Live</Name>
- <Identification>
- <ModelName>WD TV</ModelName>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="alphanetworks" match="Substring" />
- <HttpHeaderInfo name="User-Agent" value="ALPHA Networks" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>5</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>true</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="avi" audioCodec="ac3,eac3,dca,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video,mpeg4,h264,vc1" type="Video" />
- <DirectPlayProfile container="mpeg" audioCodec="ac3,eac3,dca,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video" type="Video" />
- <DirectPlayProfile container="mkv" audioCodec="ac3,eac3,dca,aac,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video,mpeg4,h264,vc1" type="Video" />
- <DirectPlayProfile container="ts,m2ts,mpegts" audioCodec="ac3,eac3,dca,mp2,mp3,aac,dts" videoCodec="mpeg1video,mpeg2video,h264,vc1" type="Video" />
- <DirectPlayProfile container="mp4,mov,m4v" audioCodec="ac3,eac3,aac,mp2,mp3,dca,dts" videoCodec="h264,mpeg4" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro" videoCodec="vc1" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="mp2,ac3" videoCodec="mpeg2video" type="Video" />
- <DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />
- <DirectPlayProfile container="mp4" audioCodec="mp4" type="Audio" />
- <DirectPlayProfile container="flac" type="Audio" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
- <DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" />
- <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Photo">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="ts,mpegts" type="Video" orgPn="MPEG_TS_SD_NA">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="External" />
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="sub" method="Embed" />
- <SubtitleProfile format="subrip" method="Embed" />
- <SubtitleProfile format="idx" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/Xbox One.xml b/Emby.Dlna/Profiles/Xml/Xbox One.xml
deleted file mode 100644
index 26a65bbd4..000000000
--- a/Emby.Dlna/Profiles/Xml/Xbox One.xml
+++ /dev/null
@@ -1,126 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>Xbox One</Name>
- <Identification>
- <ModelName>Xbox One</ModelName>
- <Headers>
- <HttpHeaderInfo name="FriendlyName.DLNA.ORG" value="XboxOne" match="Substring" />
- <HttpHeaderInfo name="User-Agent" value="NSPlayer/12" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>40</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg2video,hevc" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" />
- <DirectPlayProfile container="avi" audioCodec="aac" videoCodec="h264" type="Video" />
- <DirectPlayProfile container="mp4,mov,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4,mpeg2video,hevc" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro" videoCodec="wmv2,wmv3,vc1" type="Video" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
- <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
- <DirectPlayProfile container="jpeg" type="Photo" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" videoCodec="jpeg" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles>
- <ContainerProfile type="Video" container="mp4,mov">
- <Conditions>
- <ProfileCondition condition="Equals" property="Has64BitOffsets" value="false" isRequired="false" />
- </Conditions>
- </ContainerProfile>
- </ContainerProfiles>
- <CodecProfiles>
- <CodecProfile type="Video" codec="mpeg4">
- <Conditions>
- <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="5120000" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video" codec="h264">
- <Conditions>
- <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" />
- <ProfileCondition condition="EqualsAny" property="VideoProfile" value="high|main|baseline|constrained baseline" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video" codec="wmv2,wmv3,vc1">
- <Conditions>
- <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
- <ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="Video">
- <Conditions>
- <ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
- <ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="ac3,wmav2,wmapro">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- <CodecProfile type="VideoAudio" codec="aac">
- <Conditions>
- <ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
- <ProfileCondition condition="Equals" property="AudioProfile" value="lc" isRequired="false" />
- </Conditions>
- <ApplyConditions />
- </CodecProfile>
- </CodecProfiles>
- <ResponseProfiles>
- <ResponseProfile container="avi" type="Video" mimeType="video/avi">
- <Conditions />
- </ResponseProfile>
- <ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
- <Conditions />
- </ResponseProfile>
- </ResponseProfiles>
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Profiles/Xml/foobar2000.xml b/Emby.Dlna/Profiles/Xml/foobar2000.xml
deleted file mode 100644
index 5ce75ace5..000000000
--- a/Emby.Dlna/Profiles/Xml/foobar2000.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-<?xml version="1.0"?>
-<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <Name>foobar2000</Name>
- <Identification>
- <FriendlyName>foobar</FriendlyName>
- <Headers>
- <HttpHeaderInfo name="User-Agent" value="foobar" match="Substring" />
- </Headers>
- </Identification>
- <Manufacturer>Jellyfin</Manufacturer>
- <ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
- <ModelName>Jellyfin Server</ModelName>
- <ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
- <ModelNumber>01</ModelNumber>
- <ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
- <EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
- <EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
- <EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
- <SupportedMediaTypes>Audio</SupportedMediaTypes>
- <AlbumArtPn>JPEG_SM</AlbumArtPn>
- <MaxAlbumArtWidth>480</MaxAlbumArtWidth>
- <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
- <MaxIconWidth>48</MaxIconWidth>
- <MaxIconHeight>48</MaxIconHeight>
- <MaxStreamingBitrate>140000000</MaxStreamingBitrate>
- <MaxStaticBitrate>140000000</MaxStaticBitrate>
- <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
- <MaxStaticMusicBitrate xsi:nil="true" />
- <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
- <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
- <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
- <RequiresPlainFolders>false</RequiresPlainFolders>
- <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
- <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
- <XmlRootAttributes />
- <DirectPlayProfiles>
- <DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />
- <DirectPlayProfile container="mp4" audioCodec="mp4" type="Audio" />
- <DirectPlayProfile container="aac,wav" type="Audio" />
- <DirectPlayProfile container="flac" audioCodec="flac" type="Audio" />
- <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
- <DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" />
- </DirectPlayProfiles>
- <TranscodingProfiles>
- <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
- </TranscodingProfiles>
- <ContainerProfiles />
- <CodecProfiles />
- <ResponseProfiles />
- <SubtitleProfiles>
- <SubtitleProfile format="srt" method="External" />
- <SubtitleProfile format="sub" method="External" />
- <SubtitleProfile format="srt" method="Embed" />
- <SubtitleProfile format="ass" method="Embed" />
- <SubtitleProfile format="ssa" method="Embed" />
- <SubtitleProfile format="smi" method="Embed" />
- <SubtitleProfile format="dvdsub" method="Embed" />
- <SubtitleProfile format="pgs" method="Embed" />
- <SubtitleProfile format="pgssub" method="Embed" />
- <SubtitleProfile format="sub" method="Embed" />
- <SubtitleProfile format="subrip" method="Embed" />
- <SubtitleProfile format="vtt" method="Embed" />
- </SubtitleProfiles>
-</Profile>
diff --git a/Emby.Dlna/Properties/AssemblyInfo.cs b/Emby.Dlna/Properties/AssemblyInfo.cs
deleted file mode 100644
index 606ffcf4f..000000000
--- a/Emby.Dlna/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.CompilerServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Emby.Dlna")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Jellyfin Project")]
-[assembly: AssemblyProduct("Jellyfin Server")]
-[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
deleted file mode 100644
index 69ef6f645..000000000
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ /dev/null
@@ -1,358 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Security;
-using System.Text;
-using Emby.Dlna.Common;
-using MediaBrowser.Model.Dlna;
-
-namespace Emby.Dlna.Server
-{
- public class DescriptionXmlBuilder
- {
- private readonly DeviceProfile _profile;
-
- private readonly string _serverUdn;
- private readonly string _serverAddress;
- private readonly string _serverName;
- private readonly string _serverId;
-
- public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId)
- {
- ArgumentException.ThrowIfNullOrEmpty(serverUdn);
- ArgumentException.ThrowIfNullOrEmpty(serverAddress);
-
- _profile = profile;
- _serverUdn = serverUdn;
- _serverAddress = serverAddress;
- _serverName = serverName;
- _serverId = serverId;
- }
-
- public string GetXml()
- {
- var builder = new StringBuilder();
-
- builder.Append("<?xml version=\"1.0\"?>");
-
- builder.Append("<root");
-
- var attributes = _profile.XmlRootAttributes.ToList();
-
- attributes.Insert(0, new XmlAttribute
- {
- Name = "xmlns:dlna",
- Value = "urn:schemas-dlna-org:device-1-0"
- });
- attributes.Insert(0, new XmlAttribute
- {
- Name = "xmlns",
- Value = "urn:schemas-upnp-org:device-1-0"
- });
-
- foreach (var att in attributes)
- {
- builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
- }
-
- builder.Append('>');
-
- builder.Append("<specVersion>");
- builder.Append("<major>1</major>");
- builder.Append("<minor>0</minor>");
- builder.Append("</specVersion>");
-
- AppendDeviceInfo(builder);
-
- builder.Append("</root>");
-
- return builder.ToString();
- }
-
- private void AppendDeviceInfo(StringBuilder builder)
- {
- builder.Append("<device>");
- AppendDeviceProperties(builder);
-
- AppendIconList(builder);
-
- builder.Append("<presentationURL>")
- .Append(SecurityElement.Escape(_serverAddress))
- .Append("/web/index.html</presentationURL>");
-
- AppendServiceList(builder);
- builder.Append("</device>");
- }
-
- private void AppendDeviceProperties(StringBuilder builder)
- {
- builder.Append("<dlna:X_DLNACAP/>");
-
- builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">DMS-1.50</dlna:X_DLNADOC>");
- builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">M-DMS-1.50</dlna:X_DLNADOC>");
-
- builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
-
- builder.Append("<friendlyName>")
- .Append(SecurityElement.Escape(GetFriendlyName()))
- .Append("</friendlyName>");
- builder.Append("<manufacturer>")
- .Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
- .Append("</manufacturer>");
- builder.Append("<manufacturerURL>")
- .Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
- .Append("</manufacturerURL>");
-
- builder.Append("<modelDescription>")
- .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
- .Append("</modelDescription>");
- builder.Append("<modelName>")
- .Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
- .Append("</modelName>");
-
- builder.Append("<modelNumber>")
- .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
- .Append("</modelNumber>");
- builder.Append("<modelURL>")
- .Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
- .Append("</modelURL>");
-
- if (string.IsNullOrEmpty(_profile.SerialNumber))
- {
- builder.Append("<serialNumber>")
- .Append(SecurityElement.Escape(_serverId))
- .Append("</serialNumber>");
- }
- else
- {
- builder.Append("<serialNumber>")
- .Append(SecurityElement.Escape(_profile.SerialNumber))
- .Append("</serialNumber>");
- }
-
- builder.Append("<UPC/>");
-
- builder.Append("<UDN>uuid:")
- .Append(SecurityElement.Escape(_serverUdn))
- .Append("</UDN>");
-
- if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
- {
- builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
- .Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
- .Append("</av:aggregationFlags>");
- }
- }
-
- internal string GetFriendlyName()
- {
- if (string.IsNullOrEmpty(_profile.FriendlyName))
- {
- return _serverName;
- }
-
- if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase))
- {
- return _profile.FriendlyName;
- }
-
- var characterList = new List<char>();
-
- foreach (var c in _serverName)
- {
- if (char.IsLetterOrDigit(c) || c == '-')
- {
- characterList.Add(c);
- }
- }
-
- var serverName = string.Create(
- characterList.Count,
- characterList,
- (dest, source) =>
- {
- for (int i = 0; i < dest.Length; i++)
- {
- dest[i] = source[i];
- }
- });
-
- return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
- }
-
- private void AppendIconList(StringBuilder builder)
- {
- builder.Append("<iconList>");
-
- foreach (var icon in GetIcons())
- {
- builder.Append("<icon>");
-
- builder.Append("<mimetype>")
- .Append(SecurityElement.Escape(icon.MimeType))
- .Append("</mimetype>");
- builder.Append("<width>")
- .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
- .Append("</width>");
- builder.Append("<height>")
- .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
- .Append("</height>");
- builder.Append("<depth>")
- .Append(SecurityElement.Escape(icon.Depth))
- .Append("</depth>");
- builder.Append("<url>")
- .Append(BuildUrl(icon.Url))
- .Append("</url>");
-
- builder.Append("</icon>");
- }
-
- builder.Append("</iconList>");
- }
-
- private void AppendServiceList(StringBuilder builder)
- {
- builder.Append("<serviceList>");
-
- foreach (var service in GetServices())
- {
- builder.Append("<service>");
-
- builder.Append("<serviceType>")
- .Append(SecurityElement.Escape(service.ServiceType))
- .Append("</serviceType>");
- builder.Append("<serviceId>")
- .Append(SecurityElement.Escape(service.ServiceId))
- .Append("</serviceId>");
- builder.Append("<SCPDURL>")
- .Append(BuildUrl(service.ScpdUrl))
- .Append("</SCPDURL>");
- builder.Append("<controlURL>")
- .Append(BuildUrl(service.ControlUrl))
- .Append("</controlURL>");
- builder.Append("<eventSubURL>")
- .Append(BuildUrl(service.EventSubUrl))
- .Append("</eventSubURL>");
-
- builder.Append("</service>");
- }
-
- builder.Append("</serviceList>");
- }
-
- private string BuildUrl(string url)
- {
- if (string.IsNullOrEmpty(url))
- {
- return string.Empty;
- }
-
- url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
-
- return SecurityElement.Escape(url);
- }
-
- private IEnumerable<DeviceIcon> GetIcons()
- => new[]
- {
- new DeviceIcon
- {
- MimeType = "image/png",
- Depth = "24",
- Width = 240,
- Height = 240,
- Url = "icons/logo240.png"
- },
-
- new DeviceIcon
- {
- MimeType = "image/jpeg",
- Depth = "24",
- Width = 240,
- Height = 240,
- Url = "icons/logo240.jpg"
- },
-
- new DeviceIcon
- {
- MimeType = "image/png",
- Depth = "24",
- Width = 120,
- Height = 120,
- Url = "icons/logo120.png"
- },
-
- new DeviceIcon
- {
- MimeType = "image/jpeg",
- Depth = "24",
- Width = 120,
- Height = 120,
- Url = "icons/logo120.jpg"
- },
-
- new DeviceIcon
- {
- MimeType = "image/png",
- Depth = "24",
- Width = 48,
- Height = 48,
- Url = "icons/logo48.png"
- },
-
- new DeviceIcon
- {
- MimeType = "image/jpeg",
- Depth = "24",
- Width = 48,
- Height = 48,
- Url = "icons/logo48.jpg"
- }
- };
-
- private IEnumerable<DeviceService> GetServices()
- {
- var list = new List<DeviceService>();
-
- list.Add(new DeviceService
- {
- ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1",
- ServiceId = "urn:upnp-org:serviceId:ContentDirectory",
- ScpdUrl = "contentdirectory/contentdirectory.xml",
- ControlUrl = "contentdirectory/control",
- EventSubUrl = "contentdirectory/events"
- });
-
- list.Add(new DeviceService
- {
- ServiceType = "urn:schemas-upnp-org:service:ConnectionManager:1",
- ServiceId = "urn:upnp-org:serviceId:ConnectionManager",
- ScpdUrl = "connectionmanager/connectionmanager.xml",
- ControlUrl = "connectionmanager/control",
- EventSubUrl = "connectionmanager/events"
- });
-
- if (_profile.EnableMSMediaReceiverRegistrar)
- {
- list.Add(new DeviceService
- {
- ServiceType = "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
- ServiceId = "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar",
- ScpdUrl = "mediareceiverregistrar/mediareceiverregistrar.xml",
- ControlUrl = "mediareceiverregistrar/control",
- EventSubUrl = "mediareceiverregistrar/events"
- });
- }
-
- return list;
- }
-
- public override string ToString()
- {
- return GetXml();
- }
- }
-}
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
deleted file mode 100644
index bff5307a4..000000000
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ /dev/null
@@ -1,242 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
-using System.Xml;
-using Emby.Dlna.Didl;
-using Jellyfin.Extensions;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.Service
-{
- public abstract class BaseControlHandler
- {
- private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
-
- protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
- {
- Config = config;
- Logger = logger;
- }
-
- protected IServerConfigurationManager Config { get; }
-
- protected ILogger Logger { get; }
-
- public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
- {
- try
- {
- LogRequest(request);
-
- var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false);
- LogResponse(response);
- return response;
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error processing control request");
-
- return ControlErrorHandler.GetResponse(ex);
- }
- }
-
- private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
- {
- ControlRequestInfo requestInfo;
-
- using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
- {
- var readerSettings = new XmlReaderSettings()
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true,
- Async = true
- };
-
- using var reader = XmlReader.Create(streamReader, readerSettings);
- requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
- }
-
- Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
-
- return CreateControlResponse(requestInfo);
- }
-
- private ControlResponse CreateControlResponse(ControlRequestInfo requestInfo)
- {
- var settings = new XmlWriterSettings
- {
- Encoding = Encoding.UTF8,
- CloseOutput = false
- };
-
- StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8);
-
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartDocument(true);
-
- writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
- writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
-
- writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
- writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
-
- WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
-
- writer.WriteFullEndElement();
- writer.WriteFullEndElement();
-
- writer.WriteFullEndElement();
- writer.WriteEndDocument();
- }
-
- var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
-
- var controlResponse = new ControlResponse(xml, true);
-
- controlResponse.Headers.Add("EXT", string.Empty);
-
- return controlResponse;
- }
-
- private async Task<ControlRequestInfo> ParseRequestAsync(XmlReader reader)
- {
- await reader.MoveToContentAsync().ConfigureAwait(false);
- await reader.ReadAsync().ConfigureAwait(false);
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- if (string.Equals(reader.LocalName, "Body", StringComparison.Ordinal))
- {
- if (reader.IsEmptyElement)
- {
- await reader.ReadAsync().ConfigureAwait(false);
- continue;
- }
-
- using var subReader = reader.ReadSubtree();
- return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
- }
-
- await reader.SkipAsync().ConfigureAwait(false);
- }
- else
- {
- await reader.ReadAsync().ConfigureAwait(false);
- }
- }
-
- throw new EndOfStreamException("Stream ended but no body tag found.");
- }
-
- private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
- {
- string? namespaceURI = null, localName = null;
-
- await reader.MoveToContentAsync().ConfigureAwait(false);
- await reader.ReadAsync().ConfigureAwait(false);
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- localName = reader.LocalName;
- namespaceURI = reader.NamespaceURI;
-
- if (reader.IsEmptyElement)
- {
- await reader.ReadAsync().ConfigureAwait(false);
- }
- else
- {
- var result = new ControlRequestInfo(localName, namespaceURI);
- using var subReader = reader.ReadSubtree();
- await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
- return result;
- }
- }
- else
- {
- await reader.ReadAsync().ConfigureAwait(false);
- }
- }
-
- if (localName is not null && namespaceURI is not null)
- {
- return new ControlRequestInfo(localName, namespaceURI);
- }
-
- throw new EndOfStreamException("Stream ended but no control found.");
- }
-
- private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
- {
- await reader.MoveToContentAsync().ConfigureAwait(false);
- await reader.ReadAsync().ConfigureAwait(false);
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- // TODO: Should we be doing this here, or should it be handled earlier when decoding the request?
- headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false);
- }
- else
- {
- await reader.ReadAsync().ConfigureAwait(false);
- }
- }
- }
-
- protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter);
-
- private void LogRequest(ControlRequest request)
- {
- if (!Config.GetDlnaConfiguration().EnableDebugLog)
- {
- return;
- }
-
- Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers);
- }
-
- private void LogResponse(ControlResponse response)
- {
- if (!Config.GetDlnaConfiguration().EnableDebugLog)
- {
- return;
- }
-
- Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
- }
-
- private class ControlRequestInfo
- {
- public ControlRequestInfo(string localName, string namespaceUri)
- {
- LocalName = localName;
- NamespaceURI = namespaceUri;
- Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
-
- public string LocalName { get; set; }
-
- public string NamespaceURI { get; set; }
-
- public Dictionary<string, string> Headers { get; }
- }
- }
-}
diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs
deleted file mode 100644
index 67e7bf6a6..000000000
--- a/Emby.Dlna/Service/BaseService.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System.Net.Http;
-using Emby.Dlna.Eventing;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Dlna.Service
-{
- public class BaseService : IDlnaEventManager
- {
- protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory)
- {
- Logger = logger;
- EventManager = new DlnaEventManager(logger, httpClientFactory);
- }
-
- protected IDlnaEventManager EventManager { get; }
-
- protected ILogger Logger { get; }
-
- public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
- {
- return EventManager.CancelEventSubscription(subscriptionId);
- }
-
- public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
- {
- return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
- }
-
- public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
- {
- return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
- }
- }
-}
diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs
deleted file mode 100644
index 3e2cd6d2e..000000000
--- a/Emby.Dlna/Service/ControlErrorHandler.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using System.Text;
-using System.Xml;
-using Emby.Dlna.Didl;
-
-namespace Emby.Dlna.Service
-{
- public static class ControlErrorHandler
- {
- private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
-
- public static ControlResponse GetResponse(Exception ex)
- {
- var settings = new XmlWriterSettings
- {
- Encoding = Encoding.UTF8,
- CloseOutput = false
- };
-
- StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8);
-
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartDocument(true);
-
- writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
- writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
-
- writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
- writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv);
-
- writer.WriteElementString("faultcode", "500");
- writer.WriteElementString("faultstring", ex.Message);
-
- writer.WriteStartElement("detail");
- writer.WriteRaw("<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\"><errorCode>401</errorCode><errorDescription>Invalid Action</errorDescription></UPnPError>");
- writer.WriteFullEndElement();
-
- writer.WriteFullEndElement();
- writer.WriteFullEndElement();
-
- writer.WriteFullEndElement();
- writer.WriteEndDocument();
- }
-
- return new ControlResponse(builder.ToString(), false);
- }
- }
-}
diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs
deleted file mode 100644
index 6e0bc6ad8..000000000
--- a/Emby.Dlna/Service/ServiceXmlBuilder.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Security;
-using System.Text;
-using Emby.Dlna.Common;
-
-namespace Emby.Dlna.Service
-{
- public class ServiceXmlBuilder
- {
- public string GetXml(IEnumerable<ServiceAction> actions, IEnumerable<StateVariable> stateVariables)
- {
- var builder = new StringBuilder();
-
- builder.Append("<?xml version=\"1.0\"?>");
- builder.Append("<scpd xmlns=\"urn:schemas-upnp-org:service-1-0\">");
-
- builder.Append("<specVersion>");
- builder.Append("<major>1</major>");
- builder.Append("<minor>0</minor>");
- builder.Append("</specVersion>");
-
- AppendActionList(builder, actions);
- AppendServiceStateTable(builder, stateVariables);
-
- builder.Append("</scpd>");
-
- return builder.ToString();
- }
-
- private static void AppendActionList(StringBuilder builder, IEnumerable<ServiceAction> actions)
- {
- builder.Append("<actionList>");
-
- foreach (var item in actions)
- {
- builder.Append("<action>");
-
- builder.Append("<name>")
- .Append(SecurityElement.Escape(item.Name))
- .Append("</name>");
-
- builder.Append("<argumentList>");
-
- foreach (var argument in item.ArgumentList)
- {
- builder.Append("<argument>");
-
- builder.Append("<name>")
- .Append(SecurityElement.Escape(argument.Name))
- .Append("</name>");
- builder.Append("<direction>")
- .Append(SecurityElement.Escape(argument.Direction))
- .Append("</direction>");
- builder.Append("<relatedStateVariable>")
- .Append(SecurityElement.Escape(argument.RelatedStateVariable))
- .Append("</relatedStateVariable>");
-
- builder.Append("</argument>");
- }
-
- builder.Append("</argumentList>");
-
- builder.Append("</action>");
- }
-
- builder.Append("</actionList>");
- }
-
- private static void AppendServiceStateTable(StringBuilder builder, IEnumerable<StateVariable> stateVariables)
- {
- builder.Append("<serviceStateTable>");
-
- foreach (var item in stateVariables)
- {
- var sendEvents = item.SendsEvents ? "yes" : "no";
-
- builder.Append("<stateVariable sendEvents=\"")
- .Append(sendEvents)
- .Append("\">");
-
- builder.Append("<name>")
- .Append(SecurityElement.Escape(item.Name))
- .Append("</name>");
- builder.Append("<dataType>")
- .Append(SecurityElement.Escape(item.DataType))
- .Append("</dataType>");
-
- if (item.AllowedValues.Count > 0)
- {
- builder.Append("<allowedValueList>");
- foreach (var allowedValue in item.AllowedValues)
- {
- builder.Append("<allowedValue>")
- .Append(SecurityElement.Escape(allowedValue))
- .Append("</allowedValue>");
- }
-
- builder.Append("</allowedValueList>");
- }
-
- builder.Append("</stateVariable>");
- }
-
- builder.Append("</serviceStateTable>");
- }
- }
-}
diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
deleted file mode 100644
index 4fbbc3885..000000000
--- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Events;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.Dlna;
-using Rssdp;
-using Rssdp.Infrastructure;
-
-namespace Emby.Dlna.Ssdp
-{
- public sealed class DeviceDiscovery : IDeviceDiscovery, IDisposable
- {
- private readonly object _syncLock = new object();
-
- private readonly IServerConfigurationManager _config;
-
- private SsdpDeviceLocator _deviceLocator;
- private ISsdpCommunicationsServer _commsServer;
-
- private int _listenerCount;
- private bool _disposed;
-
- public DeviceDiscovery(IServerConfigurationManager config)
- {
- _config = config;
- }
-
- private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
-
- /// <inheritdoc />
- public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered
- {
- add
- {
- lock (_syncLock)
- {
- _listenerCount++;
- DeviceDiscoveredInternal += value;
- }
-
- StartInternal();
- }
-
- remove
- {
- lock (_syncLock)
- {
- _listenerCount--;
- DeviceDiscoveredInternal -= value;
- }
- }
- }
-
- /// <inheritdoc />
- public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
-
- // Call this method from somewhere in your code to start the search.
- public void Start(ISsdpCommunicationsServer communicationsServer)
- {
- _commsServer = communicationsServer;
-
- StartInternal();
- }
-
- private void StartInternal()
- {
- lock (_syncLock)
- {
- if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null)
- {
- _deviceLocator = new SsdpDeviceLocator(
- _commsServer,
- Environment.OSVersion.Platform.ToString(),
- // Can not use VersionString here since that includes OS and version
- Environment.OSVersion.Version.ToString());
-
- // (Optional) Set the filter so we only see notifications for devices we care about
- // (can be any search target value i.e device type, uuid value etc - any value that appears in the
- // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method).
- // _DeviceLocator.NotificationFilter = "upnp:rootdevice";
-
- // Connect our event handler so we process devices as they are found
- _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable;
- _deviceLocator.DeviceUnavailable += OnDeviceLocatorDeviceUnavailable;
-
- var dueTime = TimeSpan.FromSeconds(5);
- var interval = TimeSpan.FromSeconds(_config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds);
-
- _deviceLocator.RestartBroadcastTimer(dueTime, interval);
- }
- }
- }
-
- // Process each found device in the event handler
- private void OnDeviceLocatorDeviceAvailable(object sender, DeviceAvailableEventArgs e)
- {
- var originalHeaders = e.DiscoveredDevice.ResponseHeaders;
-
- var headerDict = originalHeaders is null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase);
-
- var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
-
- var args = new GenericEventArgs<UpnpDeviceInfo>(
- new UpnpDeviceInfo
- {
- Location = e.DiscoveredDevice.DescriptionLocation,
- Headers = headers,
- RemoteIPAddress = e.RemoteIPAddress
- });
-
- DeviceDiscoveredInternal?.Invoke(this, args);
- }
-
- private void OnDeviceLocatorDeviceUnavailable(object sender, DeviceUnavailableEventArgs e)
- {
- var originalHeaders = e.DiscoveredDevice.ResponseHeaders;
-
- var headerDict = originalHeaders is null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase);
-
- var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
-
- var args = new GenericEventArgs<UpnpDeviceInfo>(
- new UpnpDeviceInfo
- {
- Location = e.DiscoveredDevice.DescriptionLocation,
- Headers = headers
- });
-
- DeviceLeft?.Invoke(this, args);
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- if (!_disposed)
- {
- _disposed = true;
- if (_deviceLocator is not null)
- {
- _deviceLocator.Dispose();
- _deviceLocator = null;
- }
- }
- }
- }
-}
diff --git a/Emby.Dlna/Ssdp/SsdpExtensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs
deleted file mode 100644
index d00eb02b4..000000000
--- a/Emby.Dlna/Ssdp/SsdpExtensions.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Linq;
-using System.Xml.Linq;
-
-namespace Emby.Dlna.Ssdp
-{
- public static class SsdpExtensions
- {
- public static string? GetValue(this XElement container, XName name)
- {
- var node = container.Element(name);
-
- return node?.Value;
- }
-
- public static string? GetAttributeValue(this XElement container, XName name)
- {
- var node = container.Attribute(name);
-
- return node?.Value;
- }
-
- public static string? GetDescendantValue(this XElement container, XName name)
- => container.Descendants(name).FirstOrDefault()?.Value;
- }
-}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 2bd089ed8..b63c8f10e 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -318,7 +318,7 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
// <!-- foo.E01., foo.e01. -->
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
- new EpisodeExpression(@"(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
+ new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
{
@@ -328,7 +328,7 @@ namespace Emby.Naming.Common
"yyyy MM dd"
}
},
- new EpisodeExpression(@"(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
+ new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
{
DateTimeFormats = new[]
{
@@ -376,7 +376,7 @@ namespace Emby.Naming.Common
IsNamed = true,
SupportsAbsoluteEpisodeNumbers = false
},
- new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$")
+ new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
{
SupportsAbsoluteEpisodeNumbers = true
},
@@ -417,7 +417,7 @@ namespace Emby.Naming.Common
},
// "1-12 episode title"
- new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
+ new EpisodeExpression("([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
@@ -712,7 +712,7 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?<chapter>[0-9]+)",
// Part if often ending of filename
- @"(?<!ch(?:apter) )(?<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.
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index f3973dad9..97015efd0 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -41,12 +41,12 @@
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
- <ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
- </ItemGroup>
-
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 0f97a0686..55dbe393c 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,19 +19,23 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 6edfad575..39524be1d 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
public abstract class BaseApplicationPaths : IApplicationPaths
{
- private string _dataPath;
-
/// <summary>
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
/// </summary>
@@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
- _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
+ DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <summary>
@@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
- public string DataPath => _dataPath;
+ public string DataPath { get; }
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index c9bf7f085..dce56e0a4 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -13,7 +13,6 @@ using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
-using Emby.Dlna.Main;
using Emby.Naming.Common;
using Emby.Photos;
using Emby.Server.Implementations.Channels;
@@ -28,7 +27,6 @@ using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
-using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
@@ -41,8 +39,8 @@ using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
-using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
+using Jellyfin.Networking.Udp;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@@ -100,6 +98,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
@@ -310,7 +309,9 @@ namespace Emby.Server.Implementations
{
_creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type);
- return ActivatorUtilities.CreateInstance(ServiceProvider, type);
+ return ServiceProvider is null
+ ? Activator.CreateInstance(type)
+ : ActivatorUtilities.CreateInstance(ServiceProvider, type);
}
catch (Exception ex)
{
@@ -865,9 +866,6 @@ namespace Emby.Server.Implementations
// MediaEncoding
yield return typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder).Assembly;
- // Dlna
- yield return typeof(DlnaEntryPoint).Assembly;
-
// Local metadata
yield return typeof(BoxSetXmlSaver).Assembly;
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 3036cbcf7..8279acb05 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1159,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
if (info.People is not null && info.People.Count > 0)
{
- _libraryManager.UpdatePeople(item, info.People);
+ await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
}
}
else if (forceUpdate)
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 77cf4089b..d0772654c 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -722,7 +722,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@IsLocked", item.IsLocked);
saveItemStatement.TryBind("@Name", item.Name);
saveItemStatement.TryBind("@OfficialRating", item.OfficialRating);
- saveItemStatement.TryBind("@MediaType", item.MediaType);
+ saveItemStatement.TryBind("@MediaType", item.MediaType.ToString());
saveItemStatement.TryBind("@Overview", item.Overview);
saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber);
saveItemStatement.TryBind("@PremiereDate", item.PremiereDate);
@@ -2042,7 +2042,7 @@ namespace Emby.Server.Implementations.Data
return false;
}
- var sortingFields = new HashSet<string>(query.OrderBy.Select(i => i.OrderBy), StringComparer.OrdinalIgnoreCase);
+ var sortingFields = new HashSet<ItemSortBy>(query.OrderBy.Select(i => i.OrderBy));
return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
|| sortingFields.Contains(ItemSortBy.IsPlayed)
@@ -2832,20 +2832,20 @@ namespace Emby.Server.Implementations.Data
if (hasSimilar || hasSearch)
{
- List<(string, SortOrder)> prepend = new List<(string, SortOrder)>(4);
+ List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4);
if (hasSearch)
{
- prepend.Add(("SearchScore", SortOrder.Descending));
+ prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending));
prepend.Add((ItemSortBy.SortName, SortOrder.Ascending));
}
if (hasSimilar)
{
- prepend.Add(("SimilarityScore", SortOrder.Descending));
+ prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending));
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
}
- var arr = new (string, SortOrder)[prepend.Count + orderBy.Count];
+ var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count];
prepend.CopyTo(arr, 0);
orderBy.CopyTo(arr, prepend.Count);
orderBy = query.OrderBy = arr;
@@ -2863,166 +2863,43 @@ namespace Emby.Server.Implementations.Data
}));
}
- private string MapOrderByField(string name, InternalItemsQuery query)
+ private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
{
- if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase))
- {
- // TODO
- return "SortName";
- }
-
- if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase))
- {
- return "RuntimeTicks";
- }
-
- if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
- {
- return "RANDOM()";
- }
-
- if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase))
- {
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return "MAX(LastPlayedDate)";
- }
-
- return "LastPlayedDate";
- }
-
- if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.PlayCount;
- }
-
- if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase))
- {
- return "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )";
- }
-
- if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.IsFolder;
- }
-
- if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase))
- {
- return "played";
- }
-
- if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase))
- {
- return "played";
- }
-
- if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase))
- {
- return "DateLastMediaAdded";
- }
-
- if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
- {
- return "InheritedParentalRatingValue";
- }
-
- if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
- {
- return "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)";
- }
-
- if (string.Equals(name, ItemSortBy.SeriesSortName, StringComparison.OrdinalIgnoreCase))
- {
- return "SeriesName";
- }
-
- if (string.Equals(name, ItemSortBy.AiredEpisodeOrder, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.AiredEpisodeOrder;
- }
-
- if (string.Equals(name, ItemSortBy.Album, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.Album;
- }
-
- if (string.Equals(name, ItemSortBy.DateCreated, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.DateCreated;
- }
-
- if (string.Equals(name, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.PremiereDate;
- }
-
- if (string.Equals(name, ItemSortBy.StartDate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.StartDate;
- }
-
- if (string.Equals(name, ItemSortBy.Name, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.Name;
- }
-
- if (string.Equals(name, ItemSortBy.CommunityRating, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.CommunityRating;
- }
-
- if (string.Equals(name, ItemSortBy.ProductionYear, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.ProductionYear;
- }
-
- if (string.Equals(name, ItemSortBy.CriticRating, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.CriticRating;
- }
-
- if (string.Equals(name, ItemSortBy.VideoBitRate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.VideoBitRate;
- }
-
- if (string.Equals(name, ItemSortBy.ParentIndexNumber, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.ParentIndexNumber;
- }
-
- if (string.Equals(name, ItemSortBy.IndexNumber, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.IndexNumber;
- }
-
- if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.SimilarityScore;
- }
-
- if (string.Equals(name, ItemSortBy.SearchScore, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.SearchScore;
- }
-
- // Unknown SortBy, just sort by the SortName.
- return ItemSortBy.SortName;
+ return sortBy switch
+ {
+ ItemSortBy.AirTime => "SortName", // TODO
+ ItemSortBy.Runtime => "RuntimeTicks",
+ ItemSortBy.Random => "RANDOM()",
+ ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)",
+ ItemSortBy.DatePlayed => "LastPlayedDate",
+ ItemSortBy.PlayCount => "PlayCount",
+ ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )",
+ ItemSortBy.IsFolder => "IsFolder",
+ ItemSortBy.IsPlayed => "played",
+ ItemSortBy.IsUnplayed => "played",
+ ItemSortBy.DateLastContentAdded => "DateLastMediaAdded",
+ ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)",
+ ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)",
+ ItemSortBy.OfficialRating => "InheritedParentalRatingValue",
+ ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)",
+ ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
+ ItemSortBy.SeriesSortName => "SeriesName",
+ ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
+ ItemSortBy.Album => "Album",
+ ItemSortBy.DateCreated => "DateCreated",
+ ItemSortBy.PremiereDate => "PremiereDate",
+ ItemSortBy.StartDate => "StartDate",
+ ItemSortBy.Name => "Name",
+ ItemSortBy.CommunityRating => "CommunityRating",
+ ItemSortBy.ProductionYear => "ProductionYear",
+ ItemSortBy.CriticRating => "CriticRating",
+ ItemSortBy.VideoBitRate => "VideoBitRate",
+ ItemSortBy.ParentIndexNumber => "ParentIndexNumber",
+ ItemSortBy.IndexNumber => "IndexNumber",
+ ItemSortBy.SimilarityScore => "SimilarityScore",
+ ItemSortBy.SearchScore => "SearchScore",
+ _ => "SortName"
+ };
}
public List<Guid> GetItemIdsList(InternalItemsQuery query)
@@ -3109,11 +2986,6 @@ namespace Emby.Server.Implementations.Data
return true;
}
- private bool IsValidMediaType(string value)
- {
- return IsAlphaNumeric(value);
- }
-
private bool IsValidPersonType(string value)
{
return IsAlphaNumeric(value);
@@ -3540,10 +3412,7 @@ namespace Emby.Server.Implementations.Data
.Append(paramName)
.Append("))) OR ");
- if (statement is not null)
- {
- statement.TryBind(paramName, query.PersonIds[i]);
- }
+ statement?.TryBind(paramName, query.PersonIds[i]);
}
clauseBuilder.Length -= Or.Length;
@@ -4124,15 +3993,14 @@ namespace Emby.Server.Implementations.Data
}
}
- var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray();
- if (queryMediaTypes.Length == 1)
+ if (query.MediaTypes.Length == 1)
{
whereClauses.Add("MediaType=@MediaTypes");
- statement?.TryBind("@MediaTypes", queryMediaTypes[0]);
+ statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString());
}
- else if (queryMediaTypes.Length > 1)
+ else if (query.MediaTypes.Length > 1)
{
- var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'"));
whereClauses.Add("MediaType in (" + val + ")");
}
@@ -4382,7 +4250,7 @@ namespace Emby.Server.Implementations.Data
foreach (var videoType in query.VideoTypes)
{
- videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'");
+ videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
}
whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 6d27703bd..44b97e8b8 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ILyricManager _lyricManager;
+ private readonly ITrickplayManager _trickplayManager;
public DtoService(
ILogger<DtoService> logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory,
- ILyricManager lyricManager)
+ ILyricManager lyricManager,
+ ITrickplayManager trickplayManager)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_lyricManager = lyricManager;
+ _trickplayManager = trickplayManager;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -1059,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto
dto.Chapters = _itemRepo.GetChapters(item);
}
+ if (options.ContainsField(ItemFields.Trickplay))
+ {
+ dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
+ }
+
if (video.ExtraType.HasValue)
{
dto.ExtraType = video.ExtraType.Value.ToString();
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 80263c139..b3344bb9f 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -14,7 +14,6 @@
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
- <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
@@ -30,7 +29,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
- <PackageReference Include="Mono.Nat" />
<PackageReference Include="prometheus-net.DotNetRuntime" />
<PackageReference Include="DotNet.Glob" />
</ItemGroup>
@@ -40,7 +38,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -49,8 +47,13 @@
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <!-- TODO: Add IDisposableAnalyzers -->
+ <!-- <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference> -->
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
deleted file mode 100644
index d6da597b8..000000000
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ /dev/null
@@ -1,208 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Networking.Configuration;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Logging;
-using Mono.Nat;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
- /// <summary>
- /// Server entrypoint handling external port forwarding.
- /// </summary>
- public class ExternalPortForwarding : IServerEntryPoint
- {
- private readonly IServerApplicationHost _appHost;
- private readonly ILogger<ExternalPortForwarding> _logger;
- private readonly IServerConfigurationManager _config;
-
- private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
-
- private Timer _timer;
- private string _configIdentifier;
-
- private bool _disposed = false;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="appHost">The application host.</param>
- /// <param name="config">The configuration manager.</param>
- public ExternalPortForwarding(
- ILogger<ExternalPortForwarding> logger,
- IServerApplicationHost appHost,
- IServerConfigurationManager config)
- {
- _logger = logger;
- _appHost = appHost;
- _config = config;
- }
-
- private string GetConfigIdentifier()
- {
- const char Separator = '|';
- var config = _config.GetNetworkConfiguration();
-
- return new StringBuilder(32)
- .Append(config.EnableUPnP).Append(Separator)
- .Append(config.PublicHttpPort).Append(Separator)
- .Append(config.PublicHttpsPort).Append(Separator)
- .Append(_appHost.HttpPort).Append(Separator)
- .Append(_appHost.HttpsPort).Append(Separator)
- .Append(_appHost.ListenWithHttps).Append(Separator)
- .Append(config.EnableRemoteAccess).Append(Separator)
- .ToString();
- }
-
- private void OnConfigurationUpdated(object sender, EventArgs e)
- {
- var oldConfigIdentifier = _configIdentifier;
- _configIdentifier = GetConfigIdentifier();
-
- if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
- {
- Stop();
- Start();
- }
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- Start();
-
- _config.ConfigurationUpdated += OnConfigurationUpdated;
-
- return Task.CompletedTask;
- }
-
- private void Start()
- {
- var config = _config.GetNetworkConfiguration();
- if (!config.EnableUPnP || !config.EnableRemoteAccess)
- {
- return;
- }
-
- _logger.LogInformation("Starting NAT discovery");
-
- NatUtility.DeviceFound += OnNatUtilityDeviceFound;
- NatUtility.StartDiscovery();
-
- _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
- }
-
- private void Stop()
- {
- _logger.LogInformation("Stopping NAT discovery");
-
- NatUtility.StopDiscovery();
- NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
-
- _timer?.Dispose();
- }
-
- private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
- {
- try
- {
- await CreateRules(e.Device).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating port forwarding rules");
- }
- }
-
- private Task CreateRules(INatDevice device)
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
-
- // On some systems the device discovered event seems to fire repeatedly
- // This check will help ensure we're not trying to port map the same device over and over
- if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
- {
- return Task.CompletedTask;
- }
-
- return Task.WhenAll(CreatePortMaps(device));
- }
-
- private IEnumerable<Task> CreatePortMaps(INatDevice device)
- {
- var config = _config.GetNetworkConfiguration();
- yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
-
- if (_appHost.ListenWithHttps)
- {
- yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
- }
- }
-
- private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
- {
- _logger.LogDebug(
- "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
- privatePort,
- publicPort,
- device.DeviceEndpoint);
-
- try
- {
- var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
- await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(
- ex,
- "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
- privatePort,
- publicPort,
- device.DeviceEndpoint);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (_disposed)
- {
- return;
- }
-
- _config.ConfigurationUpdated -= OnConfigurationUpdated;
-
- Stop();
-
- _timer = null;
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index be36bbd2c..a83d7a410 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -1,7 +1,3 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -23,476 +19,382 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.EntryPoints
+namespace Emby.Server.Implementations.EntryPoints;
+
+/// <summary>
+/// A <see cref="IServerEntryPoint"/> that notifies users when libraries are updated.
+/// </summary>
+public sealed class LibraryChangedNotifier : IServerEntryPoint
{
- public class LibraryChangedNotifier : IServerEntryPoint
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IProviderManager _providerManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly IUserManager _userManager;
+ private readonly ILogger<LibraryChangedNotifier> _logger;
+
+ private readonly object _libraryChangedSyncLock = new();
+ private readonly List<Folder> _foldersAddedTo = new();
+ private readonly List<Folder> _foldersRemovedFrom = new();
+ private readonly List<BaseItem> _itemsAdded = new();
+ private readonly List<BaseItem> _itemsRemoved = new();
+ private readonly List<BaseItem> _itemsUpdated = new();
+ private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new();
+
+ private Timer? _libraryUpdateTimer;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryChangedNotifier"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
+ /// <param name="configurationManager">The <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
+ /// <param name="userManager">The <see cref="IUserManager"/>.</param>
+ /// <param name="logger">The <see cref="ILogger"/>.</param>
+ /// <param name="providerManager">The <see cref="IProviderManager"/>.</param>
+ public LibraryChangedNotifier(
+ ILibraryManager libraryManager,
+ IServerConfigurationManager configurationManager,
+ ISessionManager sessionManager,
+ IUserManager userManager,
+ ILogger<LibraryChangedNotifier> logger,
+ IProviderManager providerManager)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IProviderManager _providerManager;
- private readonly ISessionManager _sessionManager;
- private readonly IUserManager _userManager;
- private readonly ILogger<LibraryChangedNotifier> _logger;
-
- /// <summary>
- /// The library changed sync lock.
- /// </summary>
- private readonly object _libraryChangedSyncLock = new object();
-
- private readonly List<Folder> _foldersAddedTo = new List<Folder>();
- private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
- private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
- private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
- private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
- private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new ConcurrentDictionary<Guid, DateTime>();
-
- public LibraryChangedNotifier(
- ILibraryManager libraryManager,
- IServerConfigurationManager configurationManager,
- ISessionManager sessionManager,
- IUserManager userManager,
- ILogger<LibraryChangedNotifier> logger,
- IProviderManager providerManager)
- {
- _libraryManager = libraryManager;
- _configurationManager = configurationManager;
- _sessionManager = sessionManager;
- _userManager = userManager;
- _logger = logger;
- _providerManager = providerManager;
- }
+ _libraryManager = libraryManager;
+ _configurationManager = configurationManager;
+ _sessionManager = sessionManager;
+ _userManager = userManager;
+ _logger = logger;
+ _providerManager = providerManager;
+ }
- /// <summary>
- /// Gets or sets the library update timer.
- /// </summary>
- /// <value>The library update timer.</value>
- private Timer LibraryUpdateTimer { get; set; }
+ /// <inheritdoc />
+ public Task RunAsync()
+ {
+ _libraryManager.ItemAdded += OnLibraryItemAdded;
+ _libraryManager.ItemUpdated += OnLibraryItemUpdated;
+ _libraryManager.ItemRemoved += OnLibraryItemRemoved;
- public Task RunAsync()
- {
- _libraryManager.ItemAdded += OnLibraryItemAdded;
- _libraryManager.ItemUpdated += OnLibraryItemUpdated;
- _libraryManager.ItemRemoved += OnLibraryItemRemoved;
+ _providerManager.RefreshCompleted += OnProviderRefreshCompleted;
+ _providerManager.RefreshStarted += OnProviderRefreshStarted;
+ _providerManager.RefreshProgress += OnProviderRefreshProgress;
- _providerManager.RefreshCompleted += OnProviderRefreshCompleted;
- _providerManager.RefreshStarted += OnProviderRefreshStarted;
- _providerManager.RefreshProgress += OnProviderRefreshProgress;
+ return Task.CompletedTask;
+ }
- return Task.CompletedTask;
- }
+ private void OnProviderRefreshProgress(object? sender, GenericEventArgs<Tuple<BaseItem, double>> e)
+ {
+ var item = e.Argument.Item1;
- private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
+ if (!EnableRefreshMessage(item))
{
- var item = e.Argument.Item1;
+ return;
+ }
- if (!EnableRefreshMessage(item))
+ var progress = e.Argument.Item2;
+
+ if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime))
+ {
+ if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000)
{
return;
}
+ }
- var progress = e.Argument.Item2;
+ _lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow);
- if (_lastProgressMessageTimes.TryGetValue(item.Id, out var lastMessageSendTime))
- {
- if (progress > 0 && progress < 100 && (DateTime.UtcNow - lastMessageSendTime).TotalMilliseconds < 1000)
- {
- return;
- }
- }
+ var dict = new Dictionary<string, string>();
+ dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
+ dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture);
- _lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow);
+ try
+ {
+ _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
+ }
+ catch
+ {
+ }
- var dict = new Dictionary<string, string>();
- dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
- dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture);
+ var collectionFolders = _libraryManager.GetCollectionFolders(item);
+
+ foreach (var collectionFolder in collectionFolders)
+ {
+ var collectionFolderDict = new Dictionary<string, string>
+ {
+ ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
+ ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
+ };
try
{
- _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
+ _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
}
catch
{
}
+ }
+ }
- var collectionFolders = _libraryManager.GetCollectionFolders(item);
+ private void OnProviderRefreshStarted(object? sender, GenericEventArgs<BaseItem> e)
+ {
+ OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
+ }
- foreach (var collectionFolder in collectionFolders)
- {
- var collectionFolderDict = new Dictionary<string, string>
- {
- ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
- ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
- };
-
- try
- {
- _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
- }
- catch
- {
- }
- }
- }
+ private void OnProviderRefreshCompleted(object? sender, GenericEventArgs<BaseItem> e)
+ {
+ OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
- private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
- {
- OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
- }
+ _lastProgressMessageTimes.TryRemove(e.Argument.Id, out _);
+ }
- private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
- {
- OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
+ private static bool EnableRefreshMessage(BaseItem item)
+ => item is Folder { IsRoot: false, IsTopParent: true }
+ and not (AggregateFolder or UserRootFolder or UserView or Channel);
- _lastProgressMessageTimes.TryRemove(e.Argument.Id, out _);
- }
+ private void OnLibraryItemAdded(object? sender, ItemChangeEventArgs e)
+ => OnLibraryChange(e.Item, e.Parent, _itemsAdded, _foldersAddedTo);
- private static bool EnableRefreshMessage(BaseItem item)
+ private void OnLibraryItemUpdated(object? sender, ItemChangeEventArgs e)
+ => OnLibraryChange(e.Item, e.Parent, _itemsUpdated, null);
+
+ private void OnLibraryItemRemoved(object? sender, ItemChangeEventArgs e)
+ => OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom);
+
+ private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder>? foldersList)
+ {
+ if (!FilterItem(item))
{
- if (item is not Folder folder)
- {
- return false;
- }
+ return;
+ }
- if (folder.IsRoot)
- {
- return false;
- }
+ lock (_libraryChangedSyncLock)
+ {
+ var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
- if (folder is AggregateFolder || folder is UserRootFolder)
+ if (_libraryUpdateTimer is null)
{
- return false;
+ _libraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan);
}
-
- if (folder is UserView || folder is Channel)
+ else
{
- return false;
+ _libraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan);
}
- if (!folder.IsTopParent)
+ if (foldersList is not null && parent is Folder folder)
{
- return false;
+ foldersList.Add(folder);
}
- return true;
+ itemsList.Add(item);
}
+ }
- /// <summary>
- /// Handles the ItemAdded event of the libraryManager control.
- /// </summary>
- /// <param name="sender">The source of the event.</param>
- /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
+ private async void LibraryUpdateTimerCallback(object? state)
+ {
+ List<Folder> foldersAddedTo;
+ List<Folder> foldersRemovedFrom;
+ List<BaseItem> itemsUpdated;
+ List<BaseItem> itemsAdded;
+ List<BaseItem> itemsRemoved;
+ lock (_libraryChangedSyncLock)
{
- if (!FilterItem(e.Item))
- {
- return;
- }
+ // Remove dupes in case some were saved multiple times
+ foldersAddedTo = _foldersAddedTo
+ .DistinctBy(x => x.Id)
+ .ToList();
- lock (_libraryChangedSyncLock)
+ foldersRemovedFrom = _foldersRemovedFrom
+ .DistinctBy(x => x.Id)
+ .ToList();
+
+ itemsUpdated = _itemsUpdated
+ .Where(i => !_itemsAdded.Contains(i))
+ .DistinctBy(x => x.Id)
+ .ToList();
+
+ itemsAdded = _itemsAdded.ToList();
+ itemsRemoved = _itemsRemoved.ToList();
+
+ if (_libraryUpdateTimer is not null)
{
- if (LibraryUpdateTimer is null)
- {
- LibraryUpdateTimer = new Timer(
- LibraryUpdateTimerCallback,
- null,
- TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration),
- Timeout.InfiniteTimeSpan);
- }
- else
- {
- LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
- }
-
- if (e.Item.GetParent() is Folder parent)
- {
- _foldersAddedTo.Add(parent);
- }
-
- _itemsAdded.Add(e.Item);
+ _libraryUpdateTimer.Dispose();
+ _libraryUpdateTimer = null;
}
+
+ _itemsAdded.Clear();
+ _itemsRemoved.Clear();
+ _itemsUpdated.Clear();
+ _foldersAddedTo.Clear();
+ _foldersRemovedFrom.Clear();
}
- /// <summary>
- /// Handles the ItemUpdated event of the libraryManager control.
- /// </summary>
- /// <param name="sender">The source of the event.</param>
- /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
+ await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ private async Task SendChangeNotifications(
+ List<BaseItem> itemsAdded,
+ List<BaseItem> itemsUpdated,
+ List<BaseItem> itemsRemoved,
+ List<Folder> foldersAddedTo,
+ List<Folder> foldersRemovedFrom,
+ CancellationToken cancellationToken)
+ {
+ var userIds = _sessionManager.Sessions
+ .Select(i => i.UserId)
+ .Where(i => !i.Equals(default))
+ .Distinct()
+ .ToArray();
+
+ foreach (var userId in userIds)
{
- if (!FilterItem(e.Item))
- {
- return;
- }
+ LibraryUpdateInfo info;
- lock (_libraryChangedSyncLock)
+ try
{
- if (LibraryUpdateTimer is null)
- {
- LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
- }
- else
- {
- LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
- }
-
- _itemsUpdated.Add(e.Item);
+ info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId);
}
- }
-
- /// <summary>
- /// Handles the ItemRemoved event of the libraryManager control.
- /// </summary>
- /// <param name="sender">The source of the event.</param>
- /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
- {
- if (!FilterItem(e.Item))
+ catch (Exception ex)
{
+ _logger.LogError(ex, "Error in GetLibraryUpdateInfo");
return;
}
- lock (_libraryChangedSyncLock)
+ if (info.IsEmpty)
{
- if (LibraryUpdateTimer is null)
- {
- LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
- }
- else
- {
- LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
- }
-
- if (e.Parent is Folder parent)
- {
- _foldersRemovedFrom.Add(parent);
- }
-
- _itemsRemoved.Add(e.Item);
+ continue;
}
- }
- /// <summary>
- /// Libraries the update timer callback.
- /// </summary>
- /// <param name="state">The state.</param>
- private async void LibraryUpdateTimerCallback(object state)
- {
- List<Folder> foldersAddedTo;
- List<Folder> foldersRemovedFrom;
- List<BaseItem> itemsUpdated;
- List<BaseItem> itemsAdded;
- List<BaseItem> itemsRemoved;
- lock (_libraryChangedSyncLock)
+ try
{
- // Remove dupes in case some were saved multiple times
- foldersAddedTo = _foldersAddedTo
- .DistinctBy(x => x.Id)
- .ToList();
-
- foldersRemovedFrom = _foldersRemovedFrom
- .DistinctBy(x => x.Id)
- .ToList();
-
- itemsUpdated = _itemsUpdated
- .Where(i => !_itemsAdded.Contains(i))
- .DistinctBy(x => x.Id)
- .ToList();
-
- itemsAdded = _itemsAdded.ToList();
- itemsRemoved = _itemsRemoved.ToList();
-
- if (LibraryUpdateTimer is not null)
- {
- LibraryUpdateTimer.Dispose();
- LibraryUpdateTimer = null;
- }
-
- _itemsAdded.Clear();
- _itemsRemoved.Clear();
- _itemsUpdated.Clear();
- _foldersAddedTo.Clear();
- _foldersRemovedFrom.Clear();
+ await _sessionManager.SendMessageToUserSessions(
+ new List<Guid> { userId },
+ SessionMessageType.LibraryChanged,
+ info,
+ cancellationToken)
+ .ConfigureAwait(false);
}
-
- await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Sends the change notifications.
- /// </summary>
- /// <param name="itemsAdded">The items added.</param>
- /// <param name="itemsUpdated">The items updated.</param>
- /// <param name="itemsRemoved">The items removed.</param>
- /// <param name="foldersAddedTo">The folders added to.</param>
- /// <param name="foldersRemovedFrom">The folders removed from.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- private async Task SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken)
- {
- var userIds = _sessionManager.Sessions
- .Select(i => i.UserId)
- .Where(i => !i.Equals(default))
- .Distinct()
- .ToArray();
-
- foreach (var userId in userIds)
+ catch (Exception ex)
{
- LibraryUpdateInfo info;
-
- try
- {
- info = GetLibraryUpdateInfo(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, userId);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in GetLibraryUpdateInfo");
- return;
- }
-
- if (info.IsEmpty)
- {
- continue;
- }
-
- try
- {
- await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error sending LibraryChanged message");
- }
+ _logger.LogError(ex, "Error sending LibraryChanged message");
}
}
+ }
- /// <summary>
- /// Gets the library update info.
- /// </summary>
- /// <param name="itemsAdded">The items added.</param>
- /// <param name="itemsUpdated">The items updated.</param>
- /// <param name="itemsRemoved">The items removed.</param>
- /// <param name="foldersAddedTo">The folders added to.</param>
- /// <param name="foldersRemovedFrom">The folders removed from.</param>
- /// <param name="userId">The user id.</param>
- /// <returns>LibraryUpdateInfo.</returns>
- private LibraryUpdateInfo GetLibraryUpdateInfo(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, Guid userId)
- {
- var user = _userManager.GetUserById(userId);
-
- var newAndRemoved = new List<BaseItem>();
- newAndRemoved.AddRange(foldersAddedTo);
- newAndRemoved.AddRange(foldersRemovedFrom);
-
- var allUserRootChildren = _libraryManager.GetUserRootFolder().GetChildren(user, true).OfType<Folder>().ToList();
-
- return new LibraryUpdateInfo
- {
- ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
-
- ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
-
- ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
+ private LibraryUpdateInfo GetLibraryUpdateInfo(
+ List<BaseItem> itemsAdded,
+ List<BaseItem> itemsUpdated,
+ List<BaseItem> itemsRemoved,
+ List<Folder> foldersAddedTo,
+ List<Folder> foldersRemovedFrom,
+ Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ ArgumentNullException.ThrowIfNull(user);
- FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
+ var newAndRemoved = new List<BaseItem>();
+ newAndRemoved.AddRange(foldersAddedTo);
+ newAndRemoved.AddRange(foldersRemovedFrom);
- FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(),
+ var allUserRootChildren = _libraryManager.GetUserRootFolder()
+ .GetChildren(user, true)
+ .OfType<Folder>()
+ .ToList();
- CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray()
- };
- }
-
- private static bool FilterItem(BaseItem item)
+ return new LibraryUpdateInfo
{
- if (!item.IsFolder && !item.HasPathProtocol)
- {
- return false;
- }
-
- if (item is IItemByName && item is not MusicArtist)
- {
- return false;
- }
+ ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .Distinct()
+ .ToArray(),
+ ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .Distinct()
+ .ToArray(),
+ ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true))
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .Distinct()
+ .ToArray(),
+ FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .Distinct()
+ .ToArray(),
+ FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user))
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .Distinct()
+ .ToArray(),
+ CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray()
+ };
+ }
- return item.SourceType == SourceType.Library;
+ private static bool FilterItem(BaseItem item)
+ {
+ if (!item.IsFolder && !item.HasPathProtocol)
+ {
+ return false;
}
- private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
+ if (item is IItemByName && item is not MusicArtist)
{
- var list = new List<string>();
+ return false;
+ }
- foreach (var item in items)
- {
- // If the physical root changed, return the user root
- if (item is AggregateFolder)
- {
- continue;
- }
-
- foreach (var folder in allUserRootChildren)
- {
- list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
- }
- }
+ return item.SourceType == SourceType.Library;
+ }
- return list.Distinct(StringComparer.Ordinal);
- }
+ private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
+ {
+ var list = new List<string>();
- /// <summary>
- /// Translates the physical item to user library.
- /// </summary>
- /// <typeparam name="T">The type of item.</typeparam>
- /// <param name="item">The item.</param>
- /// <param name="user">The user.</param>
- /// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>
- /// <returns>IEnumerable{``0}.</returns>
- private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
- where T : BaseItem
+ foreach (var item in items)
{
// If the physical root changed, return the user root
if (item is AggregateFolder)
{
- return new[] { _libraryManager.GetUserRootFolder() as T };
+ continue;
}
- // Return it only if it's in the user's library
- if (includeIfNotFound || item.IsVisibleStandalone(user))
+ foreach (var folder in allUserRootChildren)
{
- return new[] { item };
+ list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
}
+ }
- return Array.Empty<T>();
+ return list.Distinct(StringComparer.Ordinal);
+ }
+
+ private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
+ where T : BaseItem
+ {
+ // If the physical root changed, return the user root
+ if (item is AggregateFolder)
+ {
+ return _libraryManager.GetUserRootFolder() is T t ? new[] { t } : Array.Empty<T>();
}
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
+ // Return it only if it's in the user's library
+ if (includeIfNotFound || item.IsVisibleStandalone(user))
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ return new[] { item };
}
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
+ return Array.Empty<T>();
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ _libraryManager.ItemAdded -= OnLibraryItemAdded;
+ _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
+ _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
+
+ _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
+ _providerManager.RefreshStarted -= OnProviderRefreshStarted;
+ _providerManager.RefreshProgress -= OnProviderRefreshProgress;
+
+ if (_libraryUpdateTimer is not null)
{
- if (dispose)
- {
- if (LibraryUpdateTimer is not null)
- {
- LibraryUpdateTimer.Dispose();
- LibraryUpdateTimer = null;
- }
-
- _libraryManager.ItemAdded -= OnLibraryItemAdded;
- _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
- _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
-
- _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
- _providerManager.RefreshStarted -= OnProviderRefreshStarted;
- _providerManager.RefreshProgress -= OnProviderRefreshProgress;
- }
+ _libraryUpdateTimer.Dispose();
+ _libraryUpdateTimer = null;
}
}
}
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
deleted file mode 100644
index 2c03f9ffd..000000000
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Udp;
-using Jellyfin.Networking.Configuration;
-using Jellyfin.Networking.Extensions;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
- /// <summary>
- /// Class responsible for registering all UDP broadcast endpoints and their handlers.
- /// </summary>
- public sealed class UdpServerEntryPoint : IServerEntryPoint
- {
- /// <summary>
- /// The port of the UDP server.
- /// </summary>
- public const int PortNumber = 7359;
-
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<UdpServerEntryPoint> _logger;
- private readonly IServerApplicationHost _appHost;
- private readonly IConfiguration _config;
- private readonly IConfigurationManager _configurationManager;
- private readonly INetworkManager _networkManager;
-
- /// <summary>
- /// The UDP server.
- /// </summary>
- private List<UdpServer> _udpServers;
- private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
- private bool _disposed = false;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
- /// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
- /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- public UdpServerEntryPoint(
- ILogger<UdpServerEntryPoint> logger,
- IServerApplicationHost appHost,
- IConfiguration configuration,
- IConfigurationManager configurationManager,
- INetworkManager networkManager)
- {
- _logger = logger;
- _appHost = appHost;
- _config = configuration;
- _configurationManager = configurationManager;
- _networkManager = networkManager;
- _udpServers = new List<UdpServer>();
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- CheckDisposed();
-
- if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery)
- {
- return Task.CompletedTask;
- }
-
- try
- {
- // Linux needs to bind to the broadcast addresses to get broadcast traffic
- // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
- if (OperatingSystem.IsLinux())
- {
- // Add global broadcast listener
- var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
- server.Start(_cancellationTokenSource.Token);
- _udpServers.Add(server);
-
- // Add bind address specific broadcast listeners
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
- foreach (var intf in validInterfaces)
- {
- var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet);
- _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
-
- server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
- server.Start(_cancellationTokenSource.Token);
- _udpServers.Add(server);
- }
- }
- else
- {
- // Add bind address specific broadcast listeners
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
- foreach (var intf in validInterfaces)
- {
- var intfAddress = intf.Address;
- _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
-
- var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
- server.Start(_cancellationTokenSource.Token);
- _udpServers.Add(server);
- }
- }
- }
- catch (SocketException ex)
- {
- _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
- }
-
- return Task.CompletedTask;
- }
-
- private void CheckDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _cancellationTokenSource.Cancel();
- _cancellationTokenSource.Dispose();
- foreach (var server in _udpServers)
- {
- server.Dispose();
- }
-
- _udpServers.Clear();
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7f620d666..f83da566b 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 15b1836eb..e75cab64c 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -210,7 +210,6 @@ namespace Emby.Server.Implementations.IO
DisposeTimer();
_disposed = true;
- GC.SuppressFinalize(this);
}
}
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 4178936ce..c380d67db 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.IO
}
// unc path
- if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
+ if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
{
return filePath;
}
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index 539d4a63a..04d90af3c 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Images
Recursive = true,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
- OrderBy = new (string, SortOrder)[]
+ OrderBy = new (ItemSortBy, SortOrder)[]
{
(ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending)
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index 8a0e627b9..34c722e41 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -30,47 +30,43 @@ namespace Emby.Server.Implementations.Images
BaseItemKind[] includeItemTypes;
- if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
+ switch (viewType)
{
- includeItemTypes = new[] { BaseItemKind.Movie };
- }
- else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.Series };
- }
- else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.MusicAlbum };
- }
- else if (string.Equals(viewType, CollectionType.MusicVideos, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.MusicVideo };
- }
- else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
- }
- else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.BoxSet };
- }
- else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
- }
- else
- {
- includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
+ case CollectionType.movies:
+ includeItemTypes = new[] { BaseItemKind.Movie };
+ break;
+ case CollectionType.tvshows:
+ includeItemTypes = new[] { BaseItemKind.Series };
+ break;
+ case CollectionType.music:
+ includeItemTypes = new[] { BaseItemKind.MusicAlbum };
+ break;
+ case CollectionType.musicvideos:
+ includeItemTypes = new[] { BaseItemKind.MusicVideo };
+ break;
+ case CollectionType.books:
+ includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
+ break;
+ case CollectionType.boxsets:
+ includeItemTypes = new[] { BaseItemKind.BoxSet };
+ break;
+ case CollectionType.homevideos:
+ case CollectionType.photos:
+ includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
+ break;
+ default:
+ includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
+ break;
}
- var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
+ var recursive = viewType != CollectionType.playlists;
return view.GetItemList(new InternalItemsQuery
{
CollapseBoxSetItems = false,
Recursive = recursive,
DtoOptions = new DtoOptions(false),
- ImageTypes = new ImageType[] { ImageType.Primary },
+ ImageTypes = new[] { ImageType.Primary },
Limit = 8,
OrderBy = new[]
{
diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 0bd5fdce0..6b2ae23b3 100644
--- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images
var view = (UserView)item;
var isUsingCollectionStrip = IsUsingCollectionStrip(view);
- var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.boxsets && view.ViewType != CollectionType.playlists;
var result = view.GetItemList(new InternalItemsQuery
{
@@ -112,14 +112,14 @@ namespace Emby.Server.Implementations.Images
private static bool IsUsingCollectionStrip(UserView view)
{
- string[] collectionStripViewTypes =
+ CollectionType[] collectionStripViewTypes =
{
- CollectionType.Movies,
- CollectionType.TvShows,
- CollectionType.Playlists
+ CollectionType.movies,
+ CollectionType.tvshows,
+ CollectionType.playlists
};
- return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty);
+ return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value);
}
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index ce8b1f918..a79ffd9cb 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -525,14 +525,14 @@ namespace Emby.Server.Implementations.Library
IDirectoryService directoryService,
IItemResolver[] resolvers,
Folder parent = null,
- string collectionType = null,
+ CollectionType? collectionType = null,
LibraryOptions libraryOptions = null)
{
ArgumentNullException.ThrowIfNull(fileInfo);
var fullPath = fileInfo.FullName;
- if (string.IsNullOrEmpty(collectionType) && parent is not null)
+ if (collectionType is null && parent is not null)
{
collectionType = GetContentTypeOverride(fullPath, true);
}
@@ -635,7 +635,7 @@ namespace Emby.Server.Implementations.Library
return !args.ContainsFileSystemEntryByName(".ignore");
}
- public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null)
+ public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null)
{
return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
}
@@ -645,7 +645,7 @@ namespace Emby.Server.Implementations.Library
IDirectoryService directoryService,
Folder parent,
LibraryOptions libraryOptions,
- string collectionType,
+ CollectionType? collectionType,
IItemResolver[] resolvers)
{
var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
@@ -675,7 +675,7 @@ namespace Emby.Server.Implementations.Library
IReadOnlyList<FileSystemMetadata> fileList,
IDirectoryService directoryService,
Folder parent,
- string collectionType,
+ CollectionType? collectionType,
IItemResolver[] resolvers,
LibraryOptions libraryOptions)
{
@@ -1514,7 +1514,7 @@ namespace Emby.Server.Implementations.Library
{
if (item is UserView view)
{
- if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.Ordinal))
+ if (view.ViewType == CollectionType.livetv)
{
return new[] { view.Id };
}
@@ -1543,13 +1543,13 @@ namespace Emby.Server.Implementations.Library
}
// Handle grouping
- if (user is not null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
+ if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.ViewType)
&& user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
{
return GetUserRootFolder()
.GetChildren(user, true)
.OfType<CollectionFolder>()
- .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase))
+ .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
.Where(i => user.IsFolderGrouped(i.Id))
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
@@ -1678,7 +1678,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="sortBy">The sort by.</param>
/// <param name="sortOrder">The sort order.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
- public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder)
+ public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
{
var isFirst = true;
@@ -1701,7 +1701,7 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
- public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy)
+ public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
{
var isFirst = true;
@@ -1736,9 +1736,9 @@ namespace Emby.Server.Implementations.Library
/// <param name="name">The name.</param>
/// <param name="user">The user.</param>
/// <returns>IBaseItemComparer.</returns>
- private IBaseItemComparer GetComparer(string name, User user)
+ private IBaseItemComparer GetComparer(ItemSortBy name, User user)
{
- var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase));
+ var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
@@ -2065,16 +2065,16 @@ namespace Emby.Server.Implementations.Library
: collectionFolder.GetLibraryOptions();
}
- public string GetContentType(BaseItem item)
+ public CollectionType? GetContentType(BaseItem item)
{
- string configuredContentType = GetConfiguredContentType(item, false);
- if (!string.IsNullOrEmpty(configuredContentType))
+ var configuredContentType = GetConfiguredContentType(item, false);
+ if (configuredContentType is not null)
{
return configuredContentType;
}
configuredContentType = GetConfiguredContentType(item, true);
- if (!string.IsNullOrEmpty(configuredContentType))
+ if (configuredContentType is not null)
{
return configuredContentType;
}
@@ -2082,31 +2082,31 @@ namespace Emby.Server.Implementations.Library
return GetInheritedContentType(item);
}
- public string GetInheritedContentType(BaseItem item)
+ public CollectionType? GetInheritedContentType(BaseItem item)
{
var type = GetTopFolderContentType(item);
- if (!string.IsNullOrEmpty(type))
+ if (type is not null)
{
return type;
}
return item.GetParents()
.Select(GetConfiguredContentType)
- .LastOrDefault(i => !string.IsNullOrEmpty(i));
+ .LastOrDefault(i => i is not null);
}
- public string GetConfiguredContentType(BaseItem item)
+ public CollectionType? GetConfiguredContentType(BaseItem item)
{
return GetConfiguredContentType(item, false);
}
- public string GetConfiguredContentType(string path)
+ public CollectionType? GetConfiguredContentType(string path)
{
return GetContentTypeOverride(path, false);
}
- public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
+ public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
{
if (item is ICollectionFolder collectionFolder)
{
@@ -2116,16 +2116,21 @@ namespace Emby.Server.Implementations.Library
return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
}
- private string GetContentTypeOverride(string path, bool inherit)
+ private CollectionType? GetContentTypeOverride(string path, bool inherit)
{
var nameValuePair = _configurationManager.Configuration.ContentTypes
.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
|| (inherit && !string.IsNullOrEmpty(i.Name)
&& _fileSystem.ContainsSubPath(i.Name, path)));
- return nameValuePair?.Value;
+ if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
+ {
+ return collectionType;
+ }
+
+ return null;
}
- private string GetTopFolderContentType(BaseItem item)
+ private CollectionType? GetTopFolderContentType(BaseItem item)
{
if (item is null)
{
@@ -2147,13 +2152,13 @@ namespace Emby.Server.Implementations.Library
.OfType<ICollectionFolder>()
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path))
.Select(i => i.CollectionType)
- .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ .FirstOrDefault(i => i is not null);
}
public UserView GetNamedView(
User user,
string name,
- string viewType,
+ CollectionType? viewType,
string sortName)
{
return GetNamedView(user, name, Guid.Empty, viewType, sortName);
@@ -2161,13 +2166,13 @@ namespace Emby.Server.Implementations.Library
public UserView GetNamedView(
string name,
- string viewType,
+ CollectionType viewType,
string sortName)
{
var path = Path.Combine(
_configurationManager.ApplicationPaths.InternalMetadataPath,
"views",
- _fileSystem.GetValidFilename(viewType));
+ _fileSystem.GetValidFilename(viewType.ToString()));
var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
@@ -2207,13 +2212,13 @@ namespace Emby.Server.Implementations.Library
User user,
string name,
Guid parentId,
- string viewType,
+ CollectionType? viewType,
string sortName)
{
var parentIdString = parentId.Equals(default)
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
- var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
+ var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@@ -2269,7 +2274,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetShadowView(
BaseItem parent,
- string viewType,
+ CollectionType? viewType,
string sortName)
{
ArgumentNullException.ThrowIfNull(parent);
@@ -2277,7 +2282,7 @@ namespace Emby.Server.Implementations.Library
var name = parent.Name;
var parentId = parent.Id;
- var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty);
+ var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@@ -2334,7 +2339,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetNamedView(
string name,
Guid parentId,
- string viewType,
+ CollectionType? viewType,
string sortName,
string uniqueId)
{
@@ -2343,7 +2348,7 @@ namespace Emby.Server.Implementations.Library
var parentIdString = parentId.Equals(default)
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
- var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
+ var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
if (!string.IsNullOrEmpty(uniqueId))
{
idValues += uniqueId;
@@ -2378,7 +2383,7 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
- if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
+ if (viewType != item.ViewType)
{
item.ViewType = viewType;
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
@@ -2850,7 +2855,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
- File.WriteAllBytes(path, Array.Empty<byte>());
+ await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 91469dba9..96fad9bca 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using EasyCaching.Core.Configurations;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
@@ -186,11 +187,11 @@ namespace Emby.Server.Implementations.Library
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Video)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
@@ -334,11 +335,11 @@ namespace Emby.Server.Implementations.Library
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Video)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
@@ -417,9 +418,9 @@ namespace Emby.Server.Implementations.Library
public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
{
// Item would only be null if the app didn't supply ItemId as part of the live stream open request
- var mediaType = item is null ? MediaType.Video : item.MediaType;
+ var mediaType = item?.MediaType ?? MediaType.Video;
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item);
@@ -428,7 +429,7 @@ namespace Emby.Server.Implementations.Library
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
}
- else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ else if (mediaType == MediaType.Audio)
{
var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 862f144e6..dbf05c1db 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -10,11 +10,11 @@ using Emby.Naming.Audio;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Resolvers.Audio
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public MultiItemResolverResult ResolveMultiple(
Folder parent,
List<FileSystemMetadata> files,
- string collectionType,
+ CollectionType? collectionType,
IDirectoryService directoryService)
{
var result = ResolveMultipleInternal(parent, files, collectionType);
@@ -59,9 +59,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
- string collectionType)
+ CollectionType? collectionType)
{
- if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.books)
{
return ResolveMultipleAudio(parent, files, true);
}
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType();
- var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase);
+ var isBooksCollectionType = collectionType == CollectionType.books;
if (args.IsDirectory)
{
@@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null;
}
- var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
+ var isMixedCollectionType = collectionType is null;
// For conflicting extensions, give priority to videos
if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
@@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
MediaBrowser.Controller.Entities.Audio.Audio item = null;
- var isMusicCollectionType = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+ var isMusicCollectionType = collectionType == CollectionType.music;
// Use regular audio type for mixed libraries, owned items and music
if (isMixedCollectionType ||
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index bbc70701c..0bfb7fbe6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio;
using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
protected override MusicAlbum Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
- var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+ var isMusicMediaFolder = collectionType == CollectionType.music;
// If there's a collection type and it's not music, don't allow it.
if (!isMusicMediaFolder)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index c858dc53d..1bdae7f62 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -4,6 +4,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType();
- var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+ var isMusicMediaFolder = collectionType == CollectionType.music;
// If there's a collection type and it's not music, it can't be a music artist
if (!isMusicMediaFolder)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 73861ff59..464a548ab 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var collectionType = args.GetCollectionType();
// Only process items that are in a collection folder containing books
- if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
+ if (collectionType != CollectionType.books)
{
return null;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 0b65bf921..1a210e3cc 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -28,13 +29,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
private readonly IImageProcessor _imageProcessor;
- private string[] _validCollectionTypes = new[]
+ private static readonly CollectionType[] _validCollectionTypes = new[]
{
- CollectionType.Movies,
- CollectionType.HomeVideos,
- CollectionType.MusicVideos,
- CollectionType.TvShows,
- CollectionType.Photos
+ CollectionType.movies,
+ CollectionType.homevideos,
+ CollectionType.musicvideos,
+ CollectionType.tvshows,
+ CollectionType.photos
};
/// <summary>
@@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public MultiItemResolverResult ResolveMultiple(
Folder parent,
List<FileSystemMetadata> files,
- string collectionType,
+ CollectionType? collectionType,
IDirectoryService directoryService)
{
var result = ResolveMultipleInternal(parent, files, collectionType);
@@ -99,17 +100,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Video movie = null;
var files = args.GetActualFileSystemChildren().ToList();
- if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.musicvideos)
{
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
- if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.homevideos)
{
movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
- if (string.IsNullOrEmpty(collectionType))
+ if (collectionType is null)
{
// Owned items will be caught by the video extra resolver
if (args.Parent is null)
@@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
- if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.movies)
{
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
@@ -146,22 +147,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Video item = null;
- if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.musicvideos)
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
- else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ else if (collectionType == CollectionType.movies)
{
item = ResolveVideo<Movie>(args, true);
}
- else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ else if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos)
{
item = ResolveVideo<Video>(args, false);
}
- else if (string.IsNullOrEmpty(collectionType))
+ else if (collectionType is null)
{
if (args.HasParent<Series>())
{
@@ -188,25 +188,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
- string collectionType)
+ CollectionType? collectionType)
{
if (IsInvalid(parent, collectionType))
{
return null;
}
- if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ if (collectionType is CollectionType.musicvideos)
{
return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
}
- if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos)
{
return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
- if (string.IsNullOrEmpty(collectionType))
+ if (collectionType is null)
{
// Owned items should just use the plain video type
if (parent is null)
@@ -222,12 +221,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return ResolveVideos<Movie>(parent, files, false, collectionType, true);
}
- if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.movies)
{
return ResolveVideos<Movie>(parent, files, true, collectionType, true);
}
- if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.tvshows)
{
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
}
@@ -239,13 +238,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Folder parent,
IEnumerable<FileSystemMetadata> fileSystemEntries,
bool supportMultiEditions,
- string collectionType,
+ CollectionType? collectionType,
bool parseName)
where T : Video, new()
{
var files = new List<FileSystemMetadata>();
var leftOver = new List<FileSystemMetadata>();
- var hasCollectionType = !string.IsNullOrEmpty(collectionType);
+ var hasCollectionType = collectionType is not null;
// Loop through each child file/folder and see if we find a video
foreach (var child in fileSystemEntries)
@@ -398,13 +397,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// Finds a movie based on a child file system entries.
/// </summary>
/// <returns>Movie.</returns>
- private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
+ private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, CollectionType? collectionType, bool parseName)
where T : Video, new()
{
var multiDiscFolders = new List<FileSystemMetadata>();
var libraryOptions = args.LibraryOptions;
- var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos;
+ var supportPhotos = collectionType == CollectionType.homevideos && libraryOptions.EnablePhotos;
var photos = new List<FileSystemMetadata>();
// Search for a folder rip
@@ -460,8 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
- var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)
- || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase);
+ var isPhotosCollection = collectionType == CollectionType.homevideos || collectionType == CollectionType.photos;
if (!isPhotosCollection && result.Items.Count == 1)
{
var videoPath = result.Items[0].Path;
@@ -562,7 +560,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return returnVideo;
}
- private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
+ private bool IsInvalid(Folder parent, CollectionType? collectionType)
{
if (parent is not null)
{
@@ -572,12 +570,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- if (collectionType.IsEmpty)
+ if (collectionType is null)
{
return false;
}
- return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
+ return !_validCollectionTypes.Contains(collectionType.Value);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
index 7dd0ab185..c0b00caaf 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
@@ -2,6 +2,7 @@
using System;
using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -45,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection
var collectionType = args.GetCollectionType();
- if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
- || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
+ if (collectionType == CollectionType.photos
+ || (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos))
{
if (HasPhotos(args))
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index b77c6b204..0934555b2 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,9 +1,9 @@
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -61,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection
var collectionType = args.CollectionType;
- if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
- || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
+ if (collectionType == CollectionType.photos
+ || (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos))
{
if (IsImageFile(args.Path, _imageProcessor))
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 5d569009d..a50435ae6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@@ -19,10 +20,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
public class PlaylistResolver : GenericFolderResolver<Playlist>
{
- private string[] _musicPlaylistCollectionTypes =
+ private CollectionType?[] _musicPlaylistCollectionTypes =
{
- string.Empty,
- CollectionType.Music
+ null,
+ CollectionType.music
};
/// <inheritdoc/>
@@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Check if this is a music playlist file
// It should have the correct collection type and a supported file extension
- else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType))
{
var extension = Path.GetExtension(args.Path.AsSpan());
if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
index 6bb999641..3d91ed242 100644
--- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
- private string GetCollectionType(ItemResolveArgs args)
+ private CollectionType? GetCollectionType(ItemResolveArgs args)
{
return args.FileSystemChildren
.Where(i =>
@@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
})
.Select(i => _fileSystem.GetFileNameWithoutExtension(i))
- .FirstOrDefault();
+ .Select(i => Enum.TryParse<CollectionType>(i, out var collectionType) ? collectionType : (CollectionType?)null)
+ .FirstOrDefault(i => i is not null);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 392ee4c77..5fd23c9f5 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -48,9 +49,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
- if (season is not null ||
- string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
- args.HasParent<Series>())
+ if (season is not null
+ || args.GetCollectionType() == CollectionType.tvshows
+ || args.HasParent<Series>())
{
var episode = ResolveVideo<Episode>(args, false);
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index e9538a5c9..858c5b281 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var resolver = new Naming.TV.EpisodeResolver(namingOptions);
var folderName = System.IO.Path.GetFileName(path);
- var testPath = "\\\\test\\" + folderName;
+ var testPath = @"\\test\" + folderName;
var episodeInfo = resolver.Resolve(testPath, true);
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index d4f275bed..1484c34bc 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -8,6 +8,7 @@ using System.IO;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@@ -59,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
var collectionType = args.GetCollectionType();
- if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.tvshows)
{
// TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
var configuredContentType = args.GetConfiguredContentType();
- if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ if (configuredContentType != CollectionType.tvshows)
{
return new Series
{
@@ -72,7 +73,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
};
}
}
- else if (string.IsNullOrEmpty(collectionType))
+ else if (collectionType is null)
{
if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
{
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index a0a90b129..8beeb8041 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -81,6 +81,53 @@ namespace Emby.Server.Implementations.Library
});
}
+ public void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason)
+ {
+ ArgumentNullException.ThrowIfNull(user);
+ ArgumentNullException.ThrowIfNull(item);
+ ArgumentNullException.ThrowIfNull(reason);
+ ArgumentNullException.ThrowIfNull(userDataDto);
+
+ var userData = GetUserData(user, item);
+
+ if (userDataDto.PlaybackPositionTicks.HasValue)
+ {
+ userData.PlaybackPositionTicks = userDataDto.PlaybackPositionTicks.Value;
+ }
+
+ if (userDataDto.PlayCount.HasValue)
+ {
+ userData.PlayCount = userDataDto.PlayCount.Value;
+ }
+
+ if (userDataDto.IsFavorite.HasValue)
+ {
+ userData.IsFavorite = userDataDto.IsFavorite.Value;
+ }
+
+ if (userDataDto.Likes.HasValue)
+ {
+ userData.Likes = userDataDto.Likes.Value;
+ }
+
+ if (userDataDto.Played.HasValue)
+ {
+ userData.Played = userDataDto.Played.Value;
+ }
+
+ if (userDataDto.LastPlayedDate.HasValue)
+ {
+ userData.LastPlayedDate = userDataDto.LastPlayedDate.Value;
+ }
+
+ if (userDataDto.Rating.HasValue)
+ {
+ userData.Rating = userDataDto.Rating.Value;
+ }
+
+ SaveUserData(user, item, userData, reason, CancellationToken.None);
+ }
+
/// <summary>
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
/// </summary>
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 2c3dc1857..1d662ed8d 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -8,7 +8,6 @@ using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -64,8 +63,8 @@ namespace Emby.Server.Implementations.Library
var collectionFolder = folder as ICollectionFolder;
var folderViewType = collectionFolder?.CollectionType;
- // Playlist library requires special handling because the folder only refrences user playlists
- if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ // Playlist library requires special handling because the folder only references user playlists
+ if (folderViewType == CollectionType.playlists)
{
var items = folder.GetItemList(new InternalItemsQuery(user)
{
@@ -90,7 +89,7 @@ namespace Emby.Server.Implementations.Library
continue;
}
- if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (query.PresetViews.Contains(folderViewType))
{
list.Add(GetUserView(folder, folderViewType, string.Empty));
}
@@ -100,16 +99,16 @@ namespace Emby.Server.Implementations.Library
}
}
- foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows })
+ foreach (var viewType in new[] { CollectionType.movies, CollectionType.tvshows })
{
- var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType))
+ var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null)
.ToList();
if (parents.Count > 0)
{
- var localizationKey = string.Equals(viewType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ?
- "TvShows" :
- "Movies";
+ var localizationKey = viewType == CollectionType.tvshows
+ ? "TvShows"
+ : "Movies";
list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews));
}
@@ -118,7 +117,7 @@ namespace Emby.Server.Implementations.Library
if (_config.Configuration.EnableFolderView)
{
var name = _localizationManager.GetLocalizedString("Folders");
- list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty));
+ list.Add(_libraryManager.GetNamedView(name, CollectionType.folders, string.Empty));
}
if (query.IncludeExternalContent)
@@ -164,14 +163,14 @@ namespace Emby.Server.Implementations.Library
.ToArray();
}
- public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName)
+ public UserView GetUserSubViewWithName(string name, Guid parentId, CollectionType? type, string sortName)
{
var uniqueId = parentId + "subview" + type;
return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId);
}
- public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName)
+ public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName)
{
var name = _localizationManager.GetLocalizedString(localizationKey);
@@ -180,15 +179,15 @@ namespace Emby.Server.Implementations.Library
private Folder GetUserView(
List<ICollectionFolder> parents,
- string viewType,
+ CollectionType? viewType,
string localizationKey,
string sortName,
User user,
- string[] presetViews)
+ CollectionType?[] presetViews)
{
- if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
+ if (parents.Count == 1 && parents.All(i => i.CollectionType == viewType))
{
- if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase))
+ if (!presetViews.Contains(viewType))
{
return (Folder)parents[0];
}
@@ -200,7 +199,7 @@ namespace Emby.Server.Implementations.Library
return _libraryManager.GetNamedView(user, name, viewType, sortName);
}
- public UserView GetUserView(Folder parent, string viewType, string sortName)
+ public UserView GetUserView(Folder parent, CollectionType? viewType, string sortName)
{
return _libraryManager.GetShadowView(parent, viewType, sortName);
}
@@ -280,7 +279,7 @@ namespace Emby.Server.Implementations.Library
var isPlayed = request.IsPlayed;
- if (parents.OfType<ICollectionFolder>().Any(i => string.Equals(i.CollectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase)))
+ if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.music))
{
isPlayed = null;
}
@@ -306,18 +305,18 @@ namespace Emby.Server.Implementations.Library
var hasCollectionType = parents.OfType<UserView>().ToArray();
if (hasCollectionType.Length > 0)
{
- if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)))
+ if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
{
includeItemTypes = new[] { BaseItemKind.Movie };
}
- else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)))
+ else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows))
{
includeItemTypes = new[] { BaseItemKind.Episode };
}
}
}
- var mediaTypes = new List<string>();
+ var mediaTypes = new List<MediaType>();
if (includeItemTypes.Length == 0)
{
@@ -325,18 +324,18 @@ namespace Emby.Server.Implementations.Library
{
switch (parent.CollectionType)
{
- case CollectionType.Books:
+ case CollectionType.books:
mediaTypes.Add(MediaType.Book);
mediaTypes.Add(MediaType.Audio);
break;
- case CollectionType.Music:
+ case CollectionType.music:
mediaTypes.Add(MediaType.Audio);
break;
- case CollectionType.Photos:
+ case CollectionType.photos:
mediaTypes.Add(MediaType.Photo);
mediaTypes.Add(MediaType.Video);
break;
- case CollectionType.HomeVideos:
+ case CollectionType.homevideos:
mediaTypes.Add(MediaType.Photo);
mediaTypes.Add(MediaType.Video);
break;
diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
index df45793c3..89f64ee4f 100644
--- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
- MediaTypes = new string[] { MediaType.Video },
+ MediaTypes = new[] { MediaType.Video },
IncludeItemTypes = new[] { BaseItemKind.Movie },
IsVirtualItem = false,
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index ee039ff0f..db06e4784 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv
orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
}
- if (!internalQuery.OrderBy.Any(i => string.Equals(i.OrderBy, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
+ if (internalQuery.OrderBy.All(i => i.OrderBy != ItemSortBy.SortName))
{
orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending));
}
@@ -2168,7 +2168,7 @@ namespace Emby.Server.Implementations.LiveTv
public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken)
{
var name = _localization.GetLocalizedString("HeaderLiveTV");
- return _libraryManager.GetNamedView(name, CollectionType.LiveTv, name);
+ return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
}
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 1721be9e2..ff25ee585 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -17,7 +17,6 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 4f96dde44..8cd0c4ffb 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -28,7 +28,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index a8b090635..68383a554 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -44,8 +44,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult();
}
}
-
- GC.SuppressFinalize(this);
}
public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 3ae9e256b..767b94136 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -84,15 +84,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.CompletedTask;
}
- public Task Close()
+ public async Task Close()
{
EnableStreamSharing = false;
Logger.LogInformation("Closing {Type}", GetType().Name);
- LiveStreamCancellationTokenSource.Cancel();
-
- return Task.CompletedTask;
+ await LiveStreamCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
public Stream GetStream()
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 613ea117f..db5e81df5 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
diff --git a/Emby.Server.Implementations/Localization/Core/ab.json b/Emby.Server.Implementations/Localization/Core/ab.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/ab.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 9fbf364ef..ecea8df6a 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Haal keyframes vanuit video lêers om meer presiese HLS afspeellyste te maak. Dit kan lank duur.",
"TaskKeyframeExtractor": "Keyframe Ekstraktor",
"External": "Ekstern",
- "HearingImpaired": "gehoorgestremd"
+ "HearingImpaired": "gehoorgestremd",
+ "TaskRefreshTrickplayImages": "Genereer Fopspeel Beelde",
+ "TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 93d50e6e3..35387d032 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
"External": "خارجي",
- "HearingImpaired": "ضعاف السمع"
+ "HearingImpaired": "ضعاف السمع",
+ "TaskRefreshTrickplayImages": "توليد صور Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة."
}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 3af124678..05af8d8a5 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -123,5 +123,7 @@
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
- "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу."
+ "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
+ "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках."
}
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 13b99cc99..3810e8b34 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
"TaskKeyframeExtractor": "Извличане на ключови кадри",
"External": "Външен",
- "HearingImpaired": "Увреден слух"
+ "HearingImpaired": "Увреден слух",
+ "TaskRefreshTrickplayImages": "Генерирай изображение",
+ "TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки."
}
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 005926231..4724bba3b 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -123,5 +123,7 @@
"External": "বাহ্যিক",
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
- "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।"
+ "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
+ "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
+ "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index f33ea2fc9..5da33febe 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
"TaskKeyframeExtractor": "Vytahovač klíčových snímků",
"External": "Externí",
- "HearingImpaired": "Sluchově postižení"
+ "HearingImpaired": "Sluchově postižení",
+ "TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno."
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 837172a5b..092af34b6 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
"TaskKeyframeExtractor": "Udtræk af nøglebillede",
"External": "Ekstern",
- "HearingImpaired": "Hørehæmmet"
+ "HearingImpaired": "Hørehæmmet",
+ "TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
+ "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker."
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index e1c3e9de1..f1dbf3c89 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
"TaskKeyframeExtractor": "Keyframe Extraktor",
"External": "Extern",
- "HearingImpaired": "Hörgeschädigt"
+ "HearingImpaired": "Hörgeschädigt",
+ "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
+ "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken."
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index c6e2244ca..5ea6a2252 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.",
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
"External": "Εξωτερικό",
- "HearingImpaired": "Με προβλήματα ακοής"
+ "HearingImpaired": "Με προβλήματα ακοής",
+ "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 243688388..32bf89310 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskKeyframeExtractor": "Keyframe Extractor",
"External": "External",
- "HearingImpaired": "Hearing Impaired"
+ "HearingImpaired": "Hearing Impaired",
+ "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+ "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 15088384c..496ecabd3 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -112,6 +112,8 @@
"TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
"TaskRefreshPeople": "Refresh People",
"TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
+ "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+ "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
"TaskUpdatePlugins": "Update Plugins",
"TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
"TaskCleanTranscode": "Clean Transcode Directory",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 4c56f789d..fe10be308 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"External": "Externo",
- "HearingImpaired": "Discapacidad Auditiva"
+ "HearingImpaired": "Discapacidad Auditiva",
+ "TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
+ "TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas."
}
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index e91084f92..114c76c54 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -123,5 +123,7 @@
"HeaderRecordingGroups": "Grabaketa taldeak",
"Inherit": "Oinordetu",
"TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
- "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua"
+ "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua",
+ "TaskRefreshTrickplayImages": "\"Trickplay Irudiak Sortu",
+ "TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 08344abeb..cba036ff4 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
"TaskKeyframeExtractor": "Avainkuvien purkain",
"External": "Ulkoinen",
- "HearingImpaired": "Kuulorajoitteinen"
+ "HearingImpaired": "Kuulorajoitteinen",
+ "TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
+ "TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 01b3e95fc..55ee1abaa 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -123,5 +123,7 @@
"HearingImpaired": "Bingi",
"TaskKeyframeExtractor": "Tagabunot ng Keyframe",
"TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
- "External": "External"
+ "External": "External",
+ "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe",
+ "TaskRefreshTrickplayImagesDescription": "Nanggagawa ng mga trickplay prebiyu para sa mga bidyo sa pinaganang mga aklatan."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 3ee045d89..b816738c2 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
"TaskKeyframeExtractor": "Extracteur d'image clé",
"External": "Externe",
- "HearingImpaired": "Malentendants"
+ "HearingImpaired": "Malentendants",
+ "TaskRefreshTrickplayImages": "Générer des images Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index a2b429dcd..e0aff7954 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une photo a été téléversée depuis {0}",
+ "CameraImageUploadedFrom": "Une photo a été téléchargée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
@@ -16,14 +16,14 @@
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album",
- "HeaderContinueWatching": "Reprendre le visionnage",
+ "HeaderContinueWatching": "Continuer de regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
"HeaderFavoriteEpisodes": "Épisodes favoris",
"HeaderFavoriteShows": "Séries favorites",
"HeaderFavoriteSongs": "Chansons préférées",
"HeaderLiveTV": "TV en direct",
- "HeaderNextUp": "À suivre",
+ "HeaderNextUp": "Prochain à venir",
"HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
"Inherit": "Hériter",
@@ -71,7 +71,7 @@
"ScheduledTaskStartedWithName": "{0} a démarré",
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Séries",
- "Songs": "Titres",
+ "Songs": "Chansons",
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
"SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
@@ -105,7 +105,7 @@
"TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
+ "TaskRefreshLibraryDescription": "Analyse votre médiathèque pour trouver de nouveaux fichiers et actualise les métadonnées.",
"TaskRefreshLibrary": "Analyser la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
@@ -122,7 +122,9 @@
"TaskOptimizeDatabaseDescription": "Réduit les espaces vides ou inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la médiathèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
"TaskOptimizeDatabase": "Optimiser la base de données",
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
- "TaskKeyframeExtractor": "Extracteur d'image clé",
+ "TaskKeyframeExtractor": "Extracteur d'images clés",
"External": "Externe",
- "HearingImpaired": "Malentendants"
+ "HearingImpaired": "Malentendants",
+ "TaskRefreshTrickplayImages": "Générer des images Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
}
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index ac9da1dd1..b95d07d5c 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -124,5 +124,7 @@
"TaskOptimizeDatabaseDescription": "Kompromiert d Datenbank und trennt freie Speicherplatz. Durch die Ufagb cha d Leistig nach em ne Scan vor Bibliothek oder andere Ufgabe verbesseret werde.",
"HearingImpaired": "Hörgschädigti",
"TaskKeyframeExtractor": "Keyframe-Extraktor",
- "TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe."
+ "TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe.",
+ "TaskRefreshTrickplayImages": "Trickplay-Bilder erstelle",
+ "TaskRefreshTrickplayImagesDescription": "Erstellt Trickplay-Vorschaue für Video in aktivierte Bibliothèke."
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 68e9fe833..26eab392e 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.",
"TaskKeyframeExtractor": "מחלץ תמונות מפתח",
"External": "חיצוני",
- "HearingImpaired": "לקוי שמיעה"
+ "HearingImpaired": "לקוי שמיעה",
+ "TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
+ "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index 47d3eeac5..3f4dea523 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -12,17 +12,17 @@
"HeaderAlbumArtists": "एल्बम कलाकार",
"Genres": "शैली",
"Forced": "बलपूर्वक",
- "Folders": "फोल्डेरें",
+ "Folders": "फ़ोल्डरें",
"Favorites": "पसंदीदा",
- "FailedLoginAttemptWithUserName": "लॉगिन असफल हुआ, पुनः {0} से प्रयास करें",
+ "FailedLoginAttemptWithUserName": "{0} से लॉगिन असफल हुआ",
"DeviceOnlineWithName": "{0} से संयोग हो गया है",
"DeviceOfflineWithName": "{0} से संयोग विच्छिन्न हो गया है",
"Default": "प्राथमिक",
- "Collections": "संग्रह",
+ "Collections": "संग्रहों",
"ChapterNameValue": "अध्याय",
"Channels": "चैनल",
- "CameraImageUploadedFrom": "कैमरा से एक नया चित्र अपलोड किया गया है",
- "Books": "किताब",
+ "CameraImageUploadedFrom": "{0} से एक नया कैमरावाला चित्र अपलोड किया गया है",
+ "Books": "पुस्तकों",
"AuthenticationSucceededWithUserName": "सफलता से प्रमाणीकृत",
"Artists": "कलाकारों",
"Application": "एप्लिकेशन",
@@ -123,5 +123,7 @@
"TaskRefreshPeopleDescription": "आपकी मीडिया लाइब्रेरी में अभिनेताओं और निर्देशकों के लिए मेटाडेटा अपडेट करता है।",
"TaskCleanCache": "स्वच्छ कैश निर्देशिका",
"TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कॉन्फ़िगरेशन के आधार पर लापता उपशीर्षक के लिए इंटरनेट खोजता है।",
- "TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।"
+ "TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।",
+ "TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे",
+ "TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index d01295419..5bb2b7d4d 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
- "HearingImpaired": "Oštećen sluh"
+ "HearingImpaired": "Oštećen sluh",
+ "TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
+ "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 5a4a02d80..ba3d5872a 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractor": "Kulcsképkockák kibontása",
"TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
"External": "Külső",
- "HearingImpaired": "Hallássérült"
+ "HearingImpaired": "Hallássérült",
+ "TaskRefreshTrickplayImages": "Trickplay képek generálása",
+ "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz."
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 87ce07da3..78a443348 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -13,7 +13,7 @@
"HomeVideos": "Video Rumahan",
"HeaderRecordingGroups": "Grup Rekaman",
"HeaderNextUp": "Selanjutnya",
- "HeaderLiveTV": "TV Live",
+ "HeaderLiveTV": "Siaran langsung",
"HeaderFavoriteSongs": "Lagu Favorit",
"HeaderFavoriteShows": "Tayangan Favorit",
"HeaderFavoriteEpisodes": "Episode Favorit",
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Ekstrak bingkai utama dari file video untuk membuat daftar putar HLS yang lebih tepat. Tugas ini dapat berjalan untuk waktu yang lama.",
"TaskKeyframeExtractor": "Ekstraktor Bingkai Utama",
"External": "Luar",
- "HearingImpaired": "Gangguan Pendengaran"
+ "HearingImpaired": "Gangguan Pendengaran",
+ "TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan."
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 3710f03e0..a34bcc490 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
"External": "Esterno",
- "HearingImpaired": "con problemi di udito"
+ "HearingImpaired": "con problemi di udito",
+ "TaskRefreshTrickplayImages": "Genera immagini Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 7b059c68e..ab6988006 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -4,19 +4,19 @@
"Application": "アプリケーション",
"Artists": "アーティスト",
"AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
- "Books": "ブックス",
+ "Books": "ブック",
"CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
"Channels": "チャンネル",
"ChapterNameValue": "チャプター {0}",
"Collections": "コレクション",
- "DeviceOfflineWithName": "{0} が切断されました",
- "DeviceOnlineWithName": "{0} が接続されました",
- "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} によって失敗しました",
+ "DeviceOfflineWithName": "{0} が切断しました",
+ "DeviceOnlineWithName": "{0} が接続しました",
+ "FailedLoginAttemptWithUserName": "{0} からのログインに失敗しました",
"Favorites": "お気に入り",
"Folders": "フォルダー",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
- "HeaderContinueWatching": "続けて見る",
+ "HeaderContinueWatching": "再生を続ける",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
"HeaderFavoriteEpisodes": "お気に入りのエピソード",
@@ -27,22 +27,22 @@
"HeaderRecordingGroups": "レコーディンググループ",
"HomeVideos": "ホームビデオ",
"Inherit": "継承",
- "ItemAddedWithName": "{0} をライブラリに追加しました",
- "ItemRemovedWithName": "{0} をライブラリから削除しました",
+ "ItemAddedWithName": "{0} をライブラリーに追加しました",
+ "ItemRemovedWithName": "{0} をライブラリーから削除しました",
"LabelIpAddressValue": "IPアドレス: {0}",
- "LabelRunningTimeValue": "稼働時間: {0}",
+ "LabelRunningTimeValue": "時間: {0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin Server が更新されました",
- "MessageApplicationUpdatedTo": "Jellyfin Server が {0}に更新されました",
- "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました",
- "MessageServerConfigurationUpdated": "サーバー設定が更新されました",
+ "MessageApplicationUpdated": "Jellyfin Server を更新しました",
+ "MessageApplicationUpdatedTo": "Jellyfin Server を {0}に更新しました",
+ "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} を更新しました",
+ "MessageServerConfigurationUpdated": "サーバー設定を更新しました",
"MixedContent": "ミックスコンテンツ",
"Movies": "映画",
"Music": "音楽",
"MusicVideos": "ミュージックビデオ",
"NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}",
- "NameSeasonUnknown": "不明なシーズン",
+ "NameSeasonUnknown": "シーズン不明",
"NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。",
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
@@ -88,18 +88,18 @@
"UserPolicyUpdatedWithName": "ユーザーポリシーが{0}に更新されました",
"UserStartedPlayingItemWithValues": "{0} は {2}で{1} を再生しています",
"UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました",
- "ValueHasBeenAddedToLibrary": "{0}はあなたのメディアライブラリに追加されました",
+ "ValueHasBeenAddedToLibrary": "{0} をメディアライブラリーに追加しました",
"ValueSpecialEpisodeName": "スペシャル - {0}",
"VersionNumber": "バージョン {0}",
"TaskCleanLogsDescription": "{0} 日以上前のログを消去します。",
"TaskCleanLogs": "ログの掃除",
- "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータを更新します。",
- "TaskRefreshLibrary": "メディアライブラリのスキャン",
+ "TaskRefreshLibraryDescription": "メディアライブラリーをスキャンして、新しいファイルを探し、メタデータを更新します。",
+ "TaskRefreshLibrary": "メディアライブラリーをスキャン",
"TaskCleanCacheDescription": "不要なキャッシュを消去します。",
"TaskCleanCache": "キャッシュを消去",
"TasksChannelsCategory": "ネットチャンネル",
"TasksApplicationCategory": "アプリケーション",
- "TasksLibraryCategory": "ライブラリ",
+ "TasksLibraryCategory": "ライブラリー",
"TasksMaintenanceCategory": "メンテナンス",
"TaskRefreshChannelsDescription": "ネットチャンネルの情報を更新する。",
"TaskRefreshChannels": "チャンネルの更新",
@@ -107,7 +107,7 @@
"TaskCleanTranscode": "トランスコードディレクトリの削除",
"TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。",
"TaskUpdatePlugins": "プラグインの更新",
- "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータを更新します。",
+ "TaskRefreshPeopleDescription": "メディアライブラリー内の俳優や監督のメタデータを更新します。",
"TaskRefreshPeople": "俳優や監督のデータの更新",
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索する。",
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
@@ -118,10 +118,12 @@
"Undefined": "未定義",
"Forced": "強制",
"Default": "デフォルト",
- "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリのスキャン後でこのタスクを実行するとパフォーマンスが向上する可能性があります。",
+ "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリーのスキャンやその他のデータベースの更新を伴う変更の後でこのタスクを実行すると、パフォーマンスが向上します。",
"TaskOptimizeDatabase": "データベースの最適化",
"TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。",
"TaskKeyframeExtractor": "キーフレーム抽出",
"External": "外部",
- "HearingImpaired": "聴覚障害の方"
+ "HearingImpaired": "聴覚障害の方",
+ "TaskRefreshTrickplayImages": "トリックプレー画像を生成",
+ "TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index c5a93cb96..e050196bc 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -123,5 +123,8 @@
"TaskOptimizeDatabase": "Derekqordy oñtailandyru",
"TaskKeyframeExtractorDescription": "Naqtyraq HLS oynatu tızımderın jasau üşın beinefaildardan negızgı kadrlardy şyğarady. Būl tapsyrma ūzaq uaqytqa sozyluy mümkın.",
"TaskKeyframeExtractor": "Negızgı kadrlardy şyğaru",
- "External": "Syrtqy"
+ "External": "Syrtqy",
+ "TaskRefreshTrickplayImagesDescription": "Іске қосылған кітапханалардағы бейнелер үшін Trickplay алдын ала түрінде көрсетілімді жасайды.",
+ "TaskRefreshTrickplayImages": "Trickplay үшін суреттерді жасау",
+ "HearingImpaired": "Есту қабілеті нашарға"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index ce8d8fc32..e7279994b 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas",
"TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.",
"External": "Išorinis",
- "HearingImpaired": "Su klausos sutrikimais"
+ "HearingImpaired": "Su klausos sutrikimais",
+ "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
+ "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index d558cdfe0..82a071309 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -1,7 +1,7 @@
{
"ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
- "HeaderRecordingGroups": "Ierakstu Grupas",
+ "HeaderRecordingGroups": "Ierakstu grupas",
"UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
@@ -14,13 +14,13 @@
"Photos": "Attēli",
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
"LabelRunningTimeValue": "Garums: {0}",
- "Inherit": "Mantot",
+ "Inherit": "Pārmantot",
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}",
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
- "UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
+ "UserPasswordChangedWithName": "Lietotāja {0} parole tika nomainīta",
"UserOnlineFromDevice": "{0} ir tiešsaistē no {1}",
"UserOfflineFromDevice": "{0} ir atvienojies no {1}",
"UserLockedOutWithName": "Lietotājs {0} ir ticis bloķēts",
@@ -28,23 +28,23 @@
"UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
"UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
"User": "Lietotājs",
- "TvShows": "TV Raidījumi",
+ "TvShows": "TV raidījumi",
"Sync": "Sinhronizācija",
"System": "Sistēma",
"StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
"Songs": "Dziesmas",
- "Shows": "Raidījumi",
+ "Shows": "Šovi",
"PluginUpdatedWithName": "{0} tika atjaunots",
"PluginUninstalledWithName": "{0} tika noņemts",
"PluginInstalledWithName": "{0} tika uzstādīts",
"Plugin": "Paplašinājums",
- "Playlists": "Atskaņošanas Saraksti",
+ "Playlists": "Atskaņošanas saraksti",
"MixedContent": "Jaukts saturs",
- "HomeVideos": "Mājas Video",
+ "HomeVideos": "Mājas video",
"HeaderNextUp": "Nākamais",
- "ChapterNameValue": "Nodaļa {0}",
+ "ChapterNameValue": "{0}. nodaļa",
"Application": "Lietotne",
- "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
+ "NotificationOptionServerRestartRequired": "Nepieciešams servera restarts",
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
"NotificationOptionPluginUninstalled": "Paplašinājums noņemts",
"NotificationOptionPluginInstalled": "Paplašinājums uzstādīts",
@@ -56,14 +56,14 @@
"NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
"NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
"NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
- "NameSeasonUnknown": "Nezināma Sezona",
- "NameSeasonNumber": "Sezona {0}",
+ "NameSeasonUnknown": "Nezināma sezona",
+ "NameSeasonNumber": "{0}. sezona",
"NameInstallFailed": "{0} instalācija neizdevās",
"MusicVideos": "Mūzikas video",
"Music": "Mūzika",
"Movies": "Filmas",
"MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
- "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota",
"MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
"MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
"Latest": "Jaunākais",
@@ -71,57 +71,57 @@
"ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV",
- "HeaderContinueWatching": "Turpināt Skatīšanos",
- "HeaderAlbumArtists": "Albumu Izpildītāji",
+ "HeaderContinueWatching": "Turpini skatīties",
+ "HeaderAlbumArtists": "Albumu izpildītāji",
"Genres": "Žanri",
"Folders": "Mapes",
"Favorites": "Izlase",
- "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}",
- "DeviceOnlineWithName": "{0} ir pievienojies",
- "DeviceOfflineWithName": "{0} ir atvienojies",
+ "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
+ "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
+ "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas",
"Channels": "Kanāli",
- "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
+ "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
"Books": "Grāmatas",
"Artists": "Izpildītāji",
"Albums": "Albumi",
"ProviderValue": "Provider: {0}",
- "HeaderFavoriteSongs": "Dziesmu Favorīti",
- "HeaderFavoriteShows": "Raidījumu Favorīti",
- "HeaderFavoriteEpisodes": "Episožu Favorīti",
- "HeaderFavoriteArtists": "Izpildītāju Favorīti",
- "HeaderFavoriteAlbums": "Albumu Favorīti",
- "TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.",
- "TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
+ "HeaderFavoriteSongs": "Dziesmu izlase",
+ "HeaderFavoriteShows": "Raidījumu izlase",
+ "HeaderFavoriteEpisodes": "Sēriju izlase",
+ "HeaderFavoriteArtists": "Izpildītāju izlase",
+ "HeaderFavoriteAlbums": "Albumu izlase",
+ "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
+ "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka",
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
- "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
+ "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
- "TaskRefreshChannels": "Atjaunot Kanālus",
- "TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.",
- "TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
+ "TaskRefreshChannels": "Atjaunot kanālus",
+ "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
+ "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi",
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
- "TaskUpdatePlugins": "Atjaunot Paplašinājumus",
+ "TaskUpdatePlugins": "Atjaunot paplašinājumus",
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
- "TaskRefreshPeople": "Atjaunot Cilvēkus",
- "TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
- "TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
+ "TaskRefreshPeople": "Atjaunot cilvēkus",
+ "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
+ "TaskCleanLogs": "Iztīrīt logdatņu mapi",
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
- "TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
+ "TaskRefreshLibrary": "Skenēt multivides bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
- "TaskCleanCache": "Iztīrīt Kešošanas Mapi",
- "TasksChannelsCategory": "Interneta Kanāli",
+ "TaskCleanCache": "Iztīrīt kešatmiņas mapi",
+ "TasksChannelsCategory": "Interneta kanāli",
"TasksMaintenanceCategory": "Apkope",
"Forced": "Piespiedu",
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
- "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
+ "TaskCleanActivityLog": "Notīrīt darbību žurnālu",
"Undefined": "Nenoteikts",
"Default": "Noklusējuma",
- "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
+ "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
"TaskOptimizeDatabase": "Optimizēt datubāzi",
"External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem",
- "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
+ "TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
}
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index a8fb26b91..13c58e0ab 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -123,5 +123,7 @@
"DeviceOnlineWithName": "{0} कनेक्ट झाले",
"DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे",
"AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत",
- "HearingImpaired": "कर्णबधीर"
+ "HearingImpaired": "कर्णबधीर",
+ "TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा",
+ "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 5c7dec7ef..0362c2417 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.",
"TaskKeyframeExtractor": "Nøkkelbilde-uttrekker",
"External": "Ekstern",
- "HearingImpaired": "Hørselshemmet"
+ "HearingImpaired": "Hørselshemmet",
+ "TaskRefreshTrickplayImages": "Generer Trickplay bilder",
+ "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index ac7b92de6..be397f1b8 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
"TaskKeyframeExtractor": "Keyframe-uitpakker",
"External": "Extern",
- "HearingImpaired": "Slechthorend"
+ "HearingImpaired": "Slechthorend",
+ "TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
+ "TaskRefreshTrickplayImagesDescription": "Genereert trickplay-afbeeldingen voor video's in bibliotheken waarvoor dit is ingeschakeld."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index d4c15ac87..bd572b744 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -124,5 +124,7 @@
"External": "Zewnętrzny",
"TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
- "HearingImpaired": "Niedosłyszący"
+ "HearingImpaired": "Niedosłyszący",
+ "TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index b9b93b7b6..2c8c46050 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de arquivos de vídeo para criar listas de reprodução HLS mais precisas. Esta tarefa pode ser executada por um longo tempo.",
"External": "Externo",
- "HearingImpaired": "Deficiência Auditiva"
+ "HearingImpaired": "Deficiência Auditiva",
+ "TaskRefreshTrickplayImages": "Gerar imagens Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index a75182f22..92ac2681e 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -104,8 +104,8 @@
"TaskRefreshPeople": "Atualizar Pessoas",
"TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.",
"TaskCleanLogs": "Limpar a Diretoria de Logs",
- "TaskRefreshLibraryDescription": "Scannear a biblioteca de música para novos ficheiros e atualizar os metadados.",
- "TaskRefreshLibrary": "Scannear Biblioteca de Música",
+ "TaskRefreshLibraryDescription": "Analisar a biblioteca de música para novos ficheiros e atualizar os metadados.",
+ "TaskRefreshLibrary": "Analisar Biblioteca de Música",
"TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.",
"TaskRefreshChapterImages": "Extrair Imagens dos Capítulos",
"TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.",
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
"External": "Externo",
- "HearingImpaired": "Surdo"
+ "HearingImpaired": "Surdo",
+ "TaskRefreshTrickplayImages": "Gerar imagens de truques",
+ "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 2281e80c8..103393a1e 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -92,7 +92,7 @@
"Application": "Aplicação",
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
- "TasksApplicationCategory": "Aplicativo",
+ "TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskRefreshChannels": "Atualizar Canais",
@@ -123,5 +123,7 @@
"External": "Externo",
"HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
- "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo."
+ "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
+ "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
+ "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 2c10bb477..537a6d3f2 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.",
"External": "Extern",
"TaskKeyframeExtractor": "Extractor de cadre cheie",
- "HearingImpaired": "Ascultare Impară"
+ "HearingImpaired": "Ascultare Impară",
+ "TaskRefreshTrickplayImages": "Generează imagini Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index fa6c753b6..26d678a0c 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.",
"TaskKeyframeExtractor": "Извлечение ключевых кадров",
"External": "Внешние",
- "HearingImpaired": "Для слабослышащих"
+ "HearingImpaired": "Для слабослышащих",
+ "TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена."
}
diff --git a/Emby.Server.Implementations/Localization/Core/si.json b/Emby.Server.Implementations/Localization/Core/si.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/si.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index c231d76fe..43594a42e 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
"External": "Externé",
- "HearingImpaired": "Sluchovo postihnutí"
+ "HearingImpaired": "Sluchovo postihnutí",
+ "TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index d1b73a3eb..91ed11042 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Nxjerrë kornizat kryesore nga skedarët video për të krijuar lista luajtjeje më të sakta HLS. Ky veprim mund të dojë një kohë të gjatë për tu kompletuar.",
"TaskKeyframeExtractor": "Nxjerrës i kornizës kryesore",
"External": "Jashtem",
- "HearingImpaired": "Dëgjimi i dëmtuar"
+ "HearingImpaired": "Dëgjimi i dëmtuar",
+ "TaskRefreshTrickplayImages": "Krijo Imazhe Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Krijon pamje paraprake për video në bibliotekat e aktivizuara."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 785e6b226..97062deec 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.",
"TaskKeyframeExtractor": "Extraktor för nyckelbildrutor",
"External": "Extern",
- "HearingImpaired": "Hörselskadad"
+ "HearingImpaired": "Hörselskadad",
+ "TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
+ "TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 770624a8d..646d7d7a5 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -102,7 +102,7 @@
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இல் இருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
- "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+ "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்கும்படி கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்கி நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
"TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்",
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
"TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்",
"External": "வெளி",
- "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்"
+ "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
+ "TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
+ "TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 3ce928859..6a04115fa 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -23,9 +23,9 @@
"HeaderFavoriteShows": "Favori Diziler",
"HeaderFavoriteSongs": "Favori Şarkılar",
"HeaderLiveTV": "Canlı TV",
- "HeaderNextUp": "Gelecek Hafta",
+ "HeaderNextUp": "Sıradaki Bölümler",
"HeaderRecordingGroups": "Kayıt Grupları",
- "HomeVideos": "Ana Sayfa Videoları",
+ "HomeVideos": "Ana Ekran Videoları",
"Inherit": "Devral",
"ItemAddedWithName": "{0} kütüphaneye eklendi",
"ItemRemovedWithName": "{0} kütüphaneden silindi",
@@ -81,7 +81,7 @@
"User": "Kullanıcı",
"UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
"UserDeletedWithName": "{0} kullanıcısı silindi",
- "UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor",
+ "UserDownloadingItemWithValues": "{0} kullanıcısı {1} medyasını indiriyor",
"UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi",
"UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi",
"UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi",
@@ -89,10 +89,10 @@
"UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
- "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
+ "ValueHasBeenAddedToLibrary": "{0} medya kütüphanenize eklendi",
"ValueSpecialEpisodeName": "Özel - {0}",
"VersionNumber": "Sürüm {0}",
- "TaskCleanCache": "Geçici Dosya Klasörünü Temizle",
+ "TaskCleanCache": "Önbellek Dizinini Temizle",
"TasksChannelsCategory": "İnternet Kanalları",
"TasksApplicationCategory": "Uygulama",
"TasksLibraryCategory": "Kütüphane",
@@ -116,13 +116,15 @@
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
"TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle",
"TaskCleanActivityLogDescription": "Yapılandırılan tarihten daha eski olan etkinlik günlüğü girişlerini siler.",
- "Undefined": "Bilinmeyen",
+ "Undefined": "Tanımlanmadı",
"Default": "Varsayılan",
"Forced": "Zorla",
- "TaskOptimizeDatabaseDescription": "Veritabanını sıkıştırır ve boş alanı keser. Kitaplığı taradıktan sonra veya veritabanında değişiklik anlamına gelen diğer işlemleri yaptıktan sonra bu görevi çalıştırmak performansı artırabilir.",
+ "TaskOptimizeDatabaseDescription": "Veritabanını sıkıştırır ve boş alanı keser. Kütüphaneyi taradıktan sonra veya veritabanında değişiklik anlamına gelen diğer işlemleri yaptıktan sonra bu görevi çalıştırmak performansı artırabilir.",
"TaskOptimizeDatabase": "Veritabanını optimize et",
- "TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından kareleri çıkarır. Bu görev uzun bir süre çalışabilir.",
- "TaskKeyframeExtractor": "Kare Ayırt Edici",
+ "TaskKeyframeExtractorDescription": "Daha hassas HLS çalma listeleri oluşturmak için video dosyalarından ana kareleri çıkarır. Bu görev uzun bir süre çalışabilir.",
+ "TaskKeyframeExtractor": "Ana Kare Çıkarıcı",
"External": "Harici",
- "HearingImpaired": "Duyma engelli"
+ "HearingImpaired": "Duyma Engelli",
+ "TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur",
+ "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur."
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index ff77fb8c5..bd5398f08 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.",
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
"External": "Зовнішній",
- "HearingImpaired": "З порушеннями слуху"
+ "HearingImpaired": "З порушеннями слуху",
+ "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
+ "TaskRefreshTrickplayImages": "Створення Trickplay-зображень"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 03265d3fb..b88d4eeaf 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -121,8 +121,10 @@
"Default": "默认",
"TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。",
"TaskOptimizeDatabase": "优化数据库",
- "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
+ "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的 HLS 播放列表。这项任务可能需要很长时间。",
"TaskKeyframeExtractor": "关键帧提取器",
"External": "外部",
- "HearingImpaired": "听力障碍"
+ "HearingImpaired": "听力障碍",
+ "TaskRefreshTrickplayImages": "生成时间轴缩略图",
+ "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 36f4df93d..d57a2811d 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。",
"TaskKeyframeExtractor": "關鍵幀提取器",
"External": "外部",
- "HearingImpaired": "聽力障礙"
+ "HearingImpaired": "聽力障礙",
+ "TaskRefreshTrickplayImages": "生成快轉縮圖",
+ "TaskRefreshTrickplayImagesDescription": "為啟用此設定的媒體庫生成快轉縮圖。"
}
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
deleted file mode 100644
index 505984cfb..000000000
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System;
-using System.Linq;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Net.Sockets;
-using MediaBrowser.Model.Net;
-
-namespace Emby.Server.Implementations.Net
-{
- /// <summary>
- /// Factory class to create different kinds of sockets.
- /// </summary>
- public class SocketFactory : ISocketFactory
- {
- /// <inheritdoc />
- public Socket CreateUdpBroadcastSocket(int localPort)
- {
- if (localPort < 0)
- {
- throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
- }
-
- var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
- try
- {
- socket.EnableBroadcast = true;
- socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
- socket.Bind(new IPEndPoint(IPAddress.Any, localPort));
-
- return socket;
- }
- catch
- {
- socket?.Dispose();
-
- throw;
- }
- }
-
- /// <inheritdoc />
- public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
- {
- var interfaceAddress = bindInterface.Address;
- ArgumentNullException.ThrowIfNull(interfaceAddress);
-
- if (localPort < 0)
- {
- throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
- }
-
- var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
- try
- {
- socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- socket.Bind(new IPEndPoint(interfaceAddress, localPort));
-
- return socket;
- }
- catch
- {
- socket?.Dispose();
-
- throw;
- }
- }
-
- /// <inheritdoc />
- public Socket CreateUdpMulticastSocket(IPAddress multicastAddress, IPData bindInterface, int multicastTimeToLive, int localPort)
- {
- var bindIPAddress = bindInterface.Address;
- ArgumentNullException.ThrowIfNull(multicastAddress);
- ArgumentNullException.ThrowIfNull(bindIPAddress);
-
- if (multicastTimeToLive <= 0)
- {
- throw new ArgumentException("multicastTimeToLive cannot be zero or less.", nameof(multicastTimeToLive));
- }
-
- if (localPort < 0)
- {
- throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
- }
-
- var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
-
- try
- {
- socket.MulticastLoopback = false;
- socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
-
- if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
- {
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
- socket.Bind(new IPEndPoint(multicastAddress, localPort));
- }
- else
- {
- // Only create socket if interface supports multicast
- var interfaceIndex = bindInterface.Index;
- var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
-
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
- socket.Bind(new IPEndPoint(bindIPAddress, localPort));
- }
-
- return socket;
- }
- catch
- {
- socket?.Dispose();
-
- throw;
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 649c49924..d2e2fd7d5 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -74,7 +75,7 @@ namespace Emby.Server.Implementations.Playlists
throw new ArgumentException(nameof(parentFolder));
}
- if (string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is null || options.MediaType == MediaType.Unknown)
{
foreach (var itemId in options.ItemIdList)
{
@@ -84,7 +85,7 @@ namespace Emby.Server.Implementations.Playlists
throw new ArgumentException("No item exists with the supplied Id");
}
- if (!string.IsNullOrEmpty(item.MediaType))
+ if (item.MediaType != MediaType.Unknown)
{
options.MediaType = item.MediaType;
}
@@ -102,20 +103,20 @@ namespace Emby.Server.Implementations.Playlists
{
options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
.Select(i => i.MediaType)
- .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ .FirstOrDefault(i => i != MediaType.Unknown);
}
}
- if (!string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is not null && options.MediaType != MediaType.Unknown)
{
break;
}
}
}
- if (string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is null || options.MediaType == MediaType.Unknown)
{
- options.MediaType = "Audio";
+ options.MediaType = MediaType.Audio;
}
var user = _userManager.GetUserById(options.UserId);
@@ -168,7 +169,7 @@ namespace Emby.Server.Implementations.Playlists
return path;
}
- private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, string playlistMediaType, User user, DtoOptions options)
+ private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
{
var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null);
diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index d67caa52d..f65d609c7 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists
public override bool SupportsInheritedParentImages => false;
[JsonIgnore]
- public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
+ public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.playlists;
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 20793ee39..db82a2900 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -12,10 +12,11 @@ using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
-using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
@@ -37,7 +38,7 @@ namespace Emby.Server.Implementations.Plugins
private readonly List<AssemblyLoadContext> _assemblyLoadContexts;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<PluginManager> _logger;
- private readonly IApplicationHost _appHost;
+ private readonly IServerApplicationHost _appHost;
private readonly ServerConfiguration _config;
private readonly List<LocalPlugin> _plugins;
private readonly Version _minimumVersion;
@@ -48,13 +49,13 @@ namespace Emby.Server.Implementations.Plugins
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{PluginManager}"/>.</param>
- /// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
+ /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
/// <param name="config">The <see cref="ServerConfiguration"/>.</param>
/// <param name="pluginsPath">The plugin path.</param>
/// <param name="appVersion">The application version.</param>
public PluginManager(
ILogger<PluginManager> logger,
- IApplicationHost appHost,
+ IServerApplicationHost appHost,
ServerConfiguration config,
string pluginsPath,
Version appVersion)
@@ -222,7 +223,7 @@ namespace Emby.Server.Implementations.Plugins
try
{
var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator);
- instance?.RegisterServices(serviceCollection);
+ instance?.RegisterServices(serviceCollection, _appHost);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 6ad6c4cbd..d03d40863 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dto;
@@ -115,7 +116,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
try
{
- previouslyFailedImages = File.ReadAllText(failHistoryPath)
+ previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
.Split('|', StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
@@ -156,7 +157,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
string text = string.Join('|', previouslyFailedImages);
- File.WriteAllText(failHistoryPath, text);
+ await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
}
numComplete++;
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index e935f7e5e..e8e63d286 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -19,6 +19,7 @@ using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -48,6 +49,7 @@ namespace Emby.Server.Implementations.Session
public sealed class SessionManager : ISessionManager, IAsyncDisposable
{
private readonly IUserDataManager _userDataManager;
+ private readonly IServerConfigurationManager _config;
private readonly ILogger<SessionManager> _logger;
private readonly IEventManager _eventManager;
private readonly ILibraryManager _libraryManager;
@@ -63,6 +65,7 @@ namespace Emby.Server.Implementations.Session
= new(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
+ private Timer _inactiveTimer;
private DtoOptions _itemInfoDtoOptions;
private bool _disposed = false;
@@ -71,6 +74,7 @@ namespace Emby.Server.Implementations.Session
ILogger<SessionManager> logger,
IEventManager eventManager,
IUserDataManager userDataManager,
+ IServerConfigurationManager config,
ILibraryManager libraryManager,
IUserManager userManager,
IMusicManager musicManager,
@@ -84,6 +88,7 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_eventManager = eventManager;
_userDataManager = userDataManager;
+ _config = config;
_libraryManager = libraryManager;
_userManager = userManager;
_musicManager = musicManager;
@@ -369,6 +374,15 @@ namespace Emby.Server.Implementations.Session
session.LastPlaybackCheckIn = DateTime.UtcNow;
}
+ if (info.IsPaused && session.LastPausedDate is null)
+ {
+ session.LastPausedDate = DateTime.UtcNow;
+ }
+ else if (!info.IsPaused)
+ {
+ session.LastPausedDate = null;
+ }
+
session.PlayState.IsPaused = info.IsPaused;
session.PlayState.PositionTicks = info.PositionTicks;
session.PlayState.MediaSourceId = info.MediaSourceId;
@@ -536,9 +550,18 @@ namespace Emby.Server.Implementations.Session
return users;
}
- private void StartIdleCheckTimer()
+ private void StartCheckTimers()
{
_idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
+
+ if (_config.Configuration.InactiveSessionThreshold > 0)
+ {
+ _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
+ }
+ else
+ {
+ StopInactiveCheckTimer();
+ }
}
private void StopIdleCheckTimer()
@@ -550,6 +573,15 @@ namespace Emby.Server.Implementations.Session
}
}
+ private void StopInactiveCheckTimer()
+ {
+ if (_inactiveTimer is not null)
+ {
+ _inactiveTimer.Dispose();
+ _inactiveTimer = null;
+ }
+ }
+
private async void CheckForIdlePlayback(object state)
{
var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
@@ -585,13 +617,50 @@ namespace Emby.Server.Implementations.Session
playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
.ToList();
}
-
- if (playingSessions.Count == 0)
+ else
{
StopIdleCheckTimer();
}
}
+ private async void CheckForInactiveSteams(object state)
+ {
+ var inactiveSessions = Sessions.Where(i =>
+ i.NowPlayingItem is not null
+ && i.PlayState.IsPaused
+ && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionThreshold);
+
+ foreach (var session in inactiveSessions)
+ {
+ _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold);
+
+ try
+ {
+ await SendPlaystateCommand(
+ session.Id,
+ session.Id,
+ new PlaystateRequest()
+ {
+ Command = PlaystateCommand.Stop,
+ ControllingUserId = session.UserId.ToString(),
+ SeekPositionTicks = session.PlayState?.PositionTicks
+ },
+ CancellationToken.None).ConfigureAwait(true);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", session.Id);
+ }
+ }
+
+ bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
+
+ if (!playingSessions)
+ {
+ StopInactiveCheckTimer();
+ }
+ }
+
private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
{
var item = session.FullNowPlayingItem;
@@ -668,7 +737,7 @@ namespace Emby.Server.Implementations.Session
eventArgs,
_logger);
- StartIdleCheckTimer();
+ StartCheckTimers();
}
/// <summary>
@@ -762,7 +831,7 @@ namespace Emby.Server.Implementations.Session
session.StartAutomaticProgress(info);
}
- StartIdleCheckTimer();
+ StartCheckTimers();
}
private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
@@ -1384,10 +1453,15 @@ namespace Emby.Server.Implementations.Session
return AuthenticateNewSessionInternal(request, false);
}
- private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
+ internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
{
CheckDisposed();
+ ArgumentException.ThrowIfNullOrEmpty(request.App);
+ ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
+ ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
+ ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
+
User user = null;
if (!request.UserId.Equals(default))
{
@@ -1448,8 +1522,11 @@ namespace Emby.Server.Implementations.Session
return returnResult;
}
- private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
+ internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
+ // This should be validated above, but if it isn't don't delete all tokens.
+ ArgumentException.ThrowIfNullOrEmpty(deviceId);
+
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
@@ -1798,6 +1875,12 @@ namespace Emby.Server.Implementations.Session
_idleTimer = null;
}
+ if (_inactiveTimer is not null)
+ {
+ await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
+ _inactiveTimer = null;
+ }
+
await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
_deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 964004ecc..6d13c6d57 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.AiredEpisodeOrder;
+ public ItemSortBy Type => ItemSortBy.AiredEpisodeOrder;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 67a9fbd3b..65c8599e7 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -16,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.AlbumArtist;
+ public ItemSortBy Type => ItemSortBy.AlbumArtist;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 4bed0fca1..e07113655 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.Album;
+ public ItemSortBy Type => ItemSortBy.Album;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
index a8bb55e2b..f99977e5c 100644
--- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting
public class ArtistComparer : IBaseItemComparer
{
/// <inheritdoc />
- public string Name => ItemSortBy.Artist;
+ public ItemSortBy Type => ItemSortBy.Artist;
/// <inheritdoc />
public int Compare(BaseItem? x, BaseItem? y)
diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
index 5cb11ab46..9e02ea2ae 100644
--- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.CommunityRating;
+ public ItemSortBy Type => ItemSortBy.CommunityRating;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index ba1835e4f..d4a8d4689 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.CriticRating;
+ public ItemSortBy Type => ItemSortBy.CriticRating;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index 6133aaccc..b86b4432f 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.DateCreated;
+ public ItemSortBy Type => ItemSortBy.DateCreated;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index b1cb123ce..e1c26d012 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -3,6 +3,7 @@
using System;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -34,7 +35,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.DateLastContentAdded;
+ public ItemSortBy Type => ItemSortBy.DateLastContentAdded;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 453d817c7..d668c17bf 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -2,6 +2,7 @@
using System;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -36,7 +37,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.DatePlayed;
+ public ItemSortBy Type => ItemSortBy.DatePlayed;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
index 1bcaccd8a..11cad6256 100644
--- a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.IndexNumber;
+ public ItemSortBy Type => ItemSortBy.IndexNumber;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index 73e628cf7..622a341b6 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.IsFavoriteOrLiked;
+ public ItemSortBy Type => ItemSortBy.IsFavoriteOrLiked;
/// <summary>
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
index 3c5ddeefa..6f0ca59c5 100644
--- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.IsFolder;
+ public ItemSortBy Type => ItemSortBy.IsFolder;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index 7d77a8bc5..2a3e456c2 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.IsUnplayed;
+ public ItemSortBy Type => ItemSortBy.IsUnplayed;
/// <summary>
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index 926835f90..afd8ccf9f 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.IsUnplayed;
+ public ItemSortBy Type => ItemSortBy.IsUnplayed;
/// <summary>
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index 93bec4db9..72d9c7973 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.Name;
+ public ItemSortBy Type => ItemSortBy.Name;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index ce44f99a6..b4ee2c723 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Globalization;
@@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.OfficialRating;
+ public ItemSortBy Type => ItemSortBy.OfficialRating;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
index c54750843..5aeac29be 100644
--- a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.ParentIndexNumber;
+ public ItemSortBy Type => ItemSortBy.ParentIndexNumber;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 16f1b79b3..12f88bf4d 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,6 +1,7 @@
#nullable disable
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -23,7 +24,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.PlayCount;
+ public ItemSortBy Type => ItemSortBy.PlayCount;
/// <summary>
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index db86b8002..8c8b8824f 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.PremiereDate;
+ public ItemSortBy Type => ItemSortBy.PremiereDate;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index 7fd1e024d..9aec87f18 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.ProductionYear;
+ public ItemSortBy Type => ItemSortBy.ProductionYear;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index bf0168222..6f8ea5b74 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.Random;
+ public ItemSortBy Type => ItemSortBy.Random;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index 753e58324..3c096ab02 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.Runtime;
+ public ItemSortBy Type => ItemSortBy.Runtime;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index 5b6c64f63..ed42fd6d5 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.SeriesSortName;
+ public ItemSortBy Type => ItemSortBy.SeriesSortName;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index 19abafe19..314c25d12 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.SortName;
+ public ItemSortBy Type => ItemSortBy.SortName;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index 2759d20de..e0b438ef1 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.StartDate;
+ public ItemSortBy Type => ItemSortBy.StartDate;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 89d10f3d2..0edffb783 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
/// </summary>
/// <value>The name.</value>
- public string Name => ItemSortBy.Studio;
+ public ItemSortBy Type => ItemSortBy.Studio;
/// <summary>
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
index af66b62e3..c4552474c 100644
--- a/Emby.Server.Implementations/SystemManager.cs
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -1,4 +1,3 @@
-using System;
using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -67,7 +66,8 @@ public class SystemManager : ISystemManager
ServerName = _applicationHost.FriendlyName,
LocalAddress = _applicationHost.GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
- PackageName = _startupOptions.PackageName
+ PackageName = _startupOptions.PackageName,
+ CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
};
}
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
deleted file mode 100644
index 10376ed76..000000000
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using System;
-using System.Net;
-using System.Net.Sockets;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.ApiClient;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
-
-namespace Emby.Server.Implementations.Udp
-{
- /// <summary>
- /// Provides a Udp Server.
- /// </summary>
- public sealed class UdpServer : IDisposable
- {
- /// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger _logger;
- private readonly IServerApplicationHost _appHost;
- private readonly IConfiguration _config;
-
- private readonly byte[] _receiveBuffer = new byte[8192];
-
- private Socket _udpSocket;
- private IPEndPoint _endpoint;
- private bool _disposed = false;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UdpServer" /> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="appHost">The application host.</param>
- /// <param name="configuration">The configuration manager.</param>
- /// <param name="bindAddress"> The bind address.</param>
- /// <param name="port">The port.</param>
- public UdpServer(
- ILogger logger,
- IServerApplicationHost appHost,
- IConfiguration configuration,
- IPAddress bindAddress,
- int port)
- {
- _logger = logger;
- _appHost = appHost;
- _config = configuration;
-
- _endpoint = new IPEndPoint(bindAddress, port);
-
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
- {
- MulticastLoopback = false,
- };
- _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- }
-
- private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken)
- {
- string? localUrl = _config[AddressOverrideKey];
- if (string.IsNullOrEmpty(localUrl))
- {
- localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
- }
-
- if (string.IsNullOrEmpty(localUrl))
- {
- _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined.");
- return;
- }
-
- var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
-
- try
- {
- _logger.LogDebug("Sending AutoDiscovery response");
- await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
- }
- catch (SocketException ex)
- {
- _logger.LogError(ex, "Error sending response message");
- }
- }
-
- /// <summary>
- /// Starts the specified port.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
- public void Start(CancellationToken cancellationToken)
- {
- _udpSocket.Bind(_endpoint);
-
- _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
- }
-
- private async Task BeginReceiveAsync(CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- try
- {
- var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
- var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
- var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
- if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
- {
- await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
- }
- }
- catch (SocketException ex)
- {
- _logger.LogError(ex, "Failed to receive data from socket");
- }
- catch (OperationCanceledException)
- {
- _logger.LogDebug("Broadcast socket operation cancelled");
- }
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _udpSocket?.Dispose();
-
- GC.SuppressFinalize(this);
- }
- }
-}
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index c717744b1..15c4cfdf0 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -321,9 +321,15 @@ namespace Emby.Server.Implementations.Updates
}
_completedInstallationsInternal.Add(package);
- await _eventManager.PublishAsync(isUpdate
- ? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package)
- : new PluginInstalledEventArgs(package)).ConfigureAwait(false);
+
+ if (isUpdate)
+ {
+ await _eventManager.PublishAsync(new PluginUpdatedEventArgs(package)).ConfigureAwait(false);
+ }
+ else
+ {
+ await _eventManager.PublishAsync(new PluginInstalledEventArgs(package)).ConfigureAwait(false);
+ }
_applicationHost.NotifyPendingRestart();
}
@@ -551,8 +557,7 @@ namespace Emby.Server.Implementations.Updates
}
stream.Position = 0;
- using var reader = new ZipArchive(stream);
- reader.ExtractToDirectory(targetDir, true);
+ ZipFile.ExtractToDirectory(stream, targetDir, true);
// Ensure we create one or populate existing ones with missing data.
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs b/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs
deleted file mode 100644
index d3a6ac9c8..000000000
--- a/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Emby.Dlna;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Filters;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace Jellyfin.Api.Attributes;
-
-/// <inheritdoc />
-public sealed class DlnaEnabledAttribute : ActionFilterAttribute
-{
- /// <inheritdoc />
- public override void OnActionExecuting(ActionExecutingContext context)
- {
- var serverConfigurationManager = context.HttpContext.RequestServices.GetRequiredService<IServerConfigurationManager>();
-
- var enabled = serverConfigurationManager.GetDlnaConfiguration().EnableServer;
-
- if (!enabled)
- {
- context.Result = new StatusCodeResult(StatusCodes.Status503ServiceUnavailable);
- }
- }
-}
diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
deleted file mode 100644
index cbd32ed82..000000000
--- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Microsoft.AspNetCore.Mvc.Routing;
-
-namespace Jellyfin.Api.Attributes;
-
-/// <summary>
-/// Identifies an action that supports the HTTP GET method.
-/// </summary>
-public sealed class HttpSubscribeAttribute : HttpMethodAttribute
-{
- private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
- /// </summary>
- public HttpSubscribeAttribute()
- : base(_supportedMethods)
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
- /// </summary>
- /// <param name="template">The route template. May not be null.</param>
- public HttpSubscribeAttribute(string template)
- : base(_supportedMethods, template)
- => ArgumentNullException.ThrowIfNull(template);
-}
diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
deleted file mode 100644
index f4a6dcdaf..000000000
--- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Microsoft.AspNetCore.Mvc.Routing;
-
-namespace Jellyfin.Api.Attributes;
-
-/// <summary>
-/// Identifies an action that supports the HTTP GET method.
-/// </summary>
-public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute
-{
- private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
- /// </summary>
- public HttpUnsubscribeAttribute()
- : base(_supportedMethods)
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
- /// </summary>
- /// <param name="template">The route template. May not be null.</param>
- public HttpUnsubscribeAttribute(string template)
- : base(_supportedMethods, template)
- => ArgumentNullException.ThrowIfNull(template);
-}
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index bd3e7d9e3..2853e69b0 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -27,13 +27,12 @@ namespace Jellyfin.Api.Auth
/// <param name="options">Options monitor.</param>
/// <param name="logger">The logger.</param>
/// <param name="encoder">The url encoder.</param>
- /// <param name="clock">The system clock.</param>
public CustomAuthenticationHandler(
IAuthService authService,
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
- UrlEncoder encoder,
- ISystemClock clock) : base(options, logger, encoder, clock)
+ UrlEncoder encoder)
+ : base(options, logger, encoder)
{
_authService = authService;
_logger = logger.CreateLogger<CustomAuthenticationHandler>();
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index c3d02976e..a19a203b5 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -2,6 +2,7 @@ using System;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Queries;
+using MediaBrowser.Common.Api;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 991f8cbf2..3363d7bad 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index c9d2f67f9..e7d3e694a 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -95,7 +95,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -113,7 +113,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
@@ -299,7 +299,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -317,7 +317,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 968193a6f..5bc533086 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -15,7 +15,6 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// The audio controller.
/// </summary>
-// TODO: In order to authenticate this in the future, Dlna playback will require updating
public class AudioController : BaseJellyfinApiController
{
private readonly AudioHelper _audioHelper;
@@ -95,7 +94,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -147,7 +146,6 @@ public class AudioController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -260,7 +258,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -312,7 +310,6 @@ public class AudioController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 11c4ac376..fdc16ee23 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -122,7 +122,7 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 2db04afb8..2d9f1ed69 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.ModelBinders;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Model.Collections;
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 9007dfc41..8db22f7eb 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -6,6 +6,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.ConfigurationDtos;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index aa0dff212..aa200a722 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Data.Dtos;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Queries;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Devices;
diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
deleted file mode 100644
index 415385463..000000000
--- a/Jellyfin.Api/Controllers/DlnaController.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Model.Dlna;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Jellyfin.Api.Controllers;
-
-/// <summary>
-/// Dlna Controller.
-/// </summary>
-[Authorize(Policy = Policies.RequiresElevation)]
-public class DlnaController : BaseJellyfinApiController
-{
- private readonly IDlnaManager _dlnaManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DlnaController"/> class.
- /// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public DlnaController(IDlnaManager dlnaManager)
- {
- _dlnaManager = dlnaManager;
- }
-
- /// <summary>
- /// Get profile infos.
- /// </summary>
- /// <response code="200">Device profile infos returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
- [HttpGet("ProfileInfos")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
- {
- return Ok(_dlnaManager.GetProfileInfos());
- }
-
- /// <summary>
- /// Gets the default profile.
- /// </summary>
- /// <response code="200">Default device profile returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
- [HttpGet("Profiles/Default")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<DeviceProfile> GetDefaultProfile()
- {
- return _dlnaManager.GetDefaultProfile();
- }
-
- /// <summary>
- /// Gets a single profile.
- /// </summary>
- /// <param name="profileId">Profile Id.</param>
- /// <response code="200">Device profile returned.</response>
- /// <response code="404">Device profile not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
- [HttpGet("Profiles/{profileId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
- {
- var profile = _dlnaManager.GetProfile(profileId);
- if (profile is null)
- {
- return NotFound();
- }
-
- return profile;
- }
-
- /// <summary>
- /// Deletes a profile.
- /// </summary>
- /// <param name="profileId">Profile id.</param>
- /// <response code="204">Device profile deleted.</response>
- /// <response code="404">Device profile not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
- [HttpDelete("Profiles/{profileId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteProfile([FromRoute, Required] string profileId)
- {
- var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
- if (existingDeviceProfile is null)
- {
- return NotFound();
- }
-
- _dlnaManager.DeleteProfile(profileId);
- return NoContent();
- }
-
- /// <summary>
- /// Creates a profile.
- /// </summary>
- /// <param name="deviceProfile">Device profile.</param>
- /// <response code="204">Device profile created.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Profiles")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile)
- {
- _dlnaManager.CreateProfile(deviceProfile);
- return NoContent();
- }
-
- /// <summary>
- /// Updates a profile.
- /// </summary>
- /// <param name="profileId">Profile id.</param>
- /// <param name="deviceProfile">Device profile.</param>
- /// <response code="204">Device profile updated.</response>
- /// <response code="404">Device profile not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
- [HttpPost("Profiles/{profileId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile)
- {
- var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
- if (existingDeviceProfile is null)
- {
- return NotFound();
- }
-
- _dlnaManager.UpdateProfile(profileId, deviceProfile);
- return NoContent();
- }
-}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
deleted file mode 100644
index 42576934b..000000000
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ /dev/null
@@ -1,329 +0,0 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Net.Mime;
-using System.Threading.Tasks;
-using Emby.Dlna;
-using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Model.Net;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Jellyfin.Api.Controllers;
-
-/// <summary>
-/// Dlna Server Controller.
-/// </summary>
-[Route("Dlna")]
-[DlnaEnabled]
-[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
-public class DlnaServerController : BaseJellyfinApiController
-{
- private readonly IDlnaManager _dlnaManager;
- private readonly IContentDirectory _contentDirectory;
- private readonly IConnectionManager _connectionManager;
- private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
- /// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param>
- /// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param>
- /// <param name="mediaReceiverRegistrar">Instance of the <see cref="IMediaReceiverRegistrar"/> interface.</param>
- public DlnaServerController(
- IDlnaManager dlnaManager,
- IContentDirectory contentDirectory,
- IConnectionManager connectionManager,
- IMediaReceiverRegistrar mediaReceiverRegistrar)
- {
- _dlnaManager = dlnaManager;
- _contentDirectory = contentDirectory;
- _connectionManager = connectionManager;
- _mediaReceiverRegistrar = mediaReceiverRegistrar;
- }
-
- /// <summary>
- /// Get Description Xml.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Description xml returned.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
- [HttpGet("{serverId}/description")]
- [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId)
- {
- var url = GetAbsoluteUri();
- var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
- var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
- return Ok(xml);
- }
-
- /// <summary>
- /// Gets Dlna content directory xml.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Dlna content directory returned.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
- [HttpGet("{serverId}/ContentDirectory")]
- [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
- [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId)
- {
- return Ok(_contentDirectory.GetServiceXml());
- }
-
- /// <summary>
- /// Gets Dlna media receiver registrar xml.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Dlna media receiver registrar xml returned.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Dlna media receiver registrar xml.</returns>
- [HttpGet("{serverId}/MediaReceiverRegistrar")]
- [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
- [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
- {
- return Ok(_mediaReceiverRegistrar.GetServiceXml());
- }
-
- /// <summary>
- /// Gets Dlna media receiver registrar xml.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Dlna media receiver registrar xml returned.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Dlna media receiver registrar xml.</returns>
- [HttpGet("{serverId}/ConnectionManager")]
- [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
- [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult<string> GetConnectionManager([FromRoute, Required] string serverId)
- {
- return Ok(_connectionManager.GetServiceXml());
- }
-
- /// <summary>
- /// Process a content directory control request.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Request processed.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Control response.</returns>
- [HttpPost("{serverId}/ContentDirectory/Control")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
- {
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Process a connection manager control request.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Request processed.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Control response.</returns>
- [HttpPost("{serverId}/ConnectionManager/Control")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
- {
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Process a media receiver registrar control request.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Request processed.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Control response.</returns>
- [HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
- {
- return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Processes an event subscription request.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Request processed.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Event subscription response.</returns>
- [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
- [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
- [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
- {
- return ProcessEventRequest(_mediaReceiverRegistrar);
- }
-
- /// <summary>
- /// Processes an event subscription request.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Request processed.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Event subscription response.</returns>
- [HttpSubscribe("{serverId}/ContentDirectory/Events")]
- [HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
- [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
- {
- return ProcessEventRequest(_contentDirectory);
- }
-
- /// <summary>
- /// Processes an event subscription request.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Request processed.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Event subscription response.</returns>
- [HttpSubscribe("{serverId}/ConnectionManager/Events")]
- [HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
- [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
- {
- return ProcessEventRequest(_connectionManager);
- }
-
- /// <summary>
- /// Gets a server icon.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <param name="fileName">The icon filename.</param>
- /// <response code="200">Request processed.</response>
- /// <response code="404">Not Found.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>Icon stream.</returns>
- [HttpGet("{serverId}/icons/{fileName}")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [ProducesImageFile]
- public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
- {
- return GetIconInternal(fileName);
- }
-
- /// <summary>
- /// Gets a server icon.
- /// </summary>
- /// <param name="fileName">The icon filename.</param>
- /// <returns>Icon stream.</returns>
- /// <response code="200">Request processed.</response>
- /// <response code="404">Not Found.</response>
- /// <response code="503">DLNA is disabled.</response>
- [HttpGet("icons/{fileName}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [ProducesImageFile]
- public ActionResult GetIcon([FromRoute, Required] string fileName)
- {
- return GetIconInternal(fileName);
- }
-
- private ActionResult GetIconInternal(string fileName)
- {
- var icon = _dlnaManager.GetIcon(fileName);
- if (icon is null)
- {
- return NotFound();
- }
-
- return File(icon.Stream, MimeTypes.GetMimeType(fileName));
- }
-
- private string GetAbsoluteUri()
- {
- return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
- }
-
- private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
- {
- return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
- {
- InputXml = requestStream,
- TargetServerUuId = id,
- RequestedUrl = GetAbsoluteUri()
- });
- }
-
- private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
- {
- var subscriptionId = Request.Headers["SID"];
- if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
- {
- var notificationType = Request.Headers["NT"];
- var callback = Request.Headers["CALLBACK"];
- var timeoutString = Request.Headers["TIMEOUT"];
-
- if (string.IsNullOrEmpty(notificationType))
- {
- return dlnaEventManager.RenewEventSubscription(
- subscriptionId,
- notificationType,
- timeoutString,
- callback);
- }
-
- return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
- }
-
- return dlnaEventManager.CancelEventSubscription(subscriptionId);
- }
-}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 42c94c29d..9e9c610cc 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -17,8 +17,6 @@ using Jellyfin.Extensions;
using Jellyfin.MediaEncoding.Hls.Playlist;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Encoder;
@@ -49,12 +47,10 @@ public class DynamicHlsController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
- private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
@@ -67,12 +63,10 @@ public class DynamicHlsController : BaseJellyfinApiController
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
@@ -81,12 +75,10 @@ public class DynamicHlsController : BaseJellyfinApiController
public DynamicHlsController(
ILibraryManager libraryManager,
IUserManager userManager,
- IDlnaManager dlnaManager,
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
- IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper,
@@ -95,12 +87,10 @@ public class DynamicHlsController : BaseJellyfinApiController
{
_libraryManager = libraryManager;
_userManager = userManager;
- _dlnaManager = dlnaManager;
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
- _deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
@@ -176,7 +166,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -231,7 +221,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -294,8 +283,6 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _dlnaManager,
- _deviceManager,
_transcodingJobHelper,
TranscodingJobType,
cancellationToken)
@@ -410,6 +397,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
[HttpGet("Videos/{itemId}/master.m3u8")]
@@ -421,7 +409,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -467,7 +455,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableTrickplay = true)
{
var streamingRequest = new HlsVideoRequestDto
{
@@ -475,7 +464,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -521,7 +509,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
+ EnableTrickplay = enableTrickplay
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -591,7 +580,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -644,7 +633,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -757,7 +745,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -811,7 +799,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -925,7 +912,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -978,7 +965,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -1102,7 +1088,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -1158,7 +1144,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -1283,7 +1268,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -1338,7 +1323,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -1399,8 +1383,6 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _dlnaManager,
- _deviceManager,
_transcodingJobHelper,
TranscodingJobType,
cancellationTokenSource.Token)
@@ -1439,8 +1421,6 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _dlnaManager,
- _deviceManager,
_transcodingJobHelper,
TranscodingJobType,
cancellationToken)
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 8c9ee1a19..284a97621 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.Api;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Authorization;
@@ -168,7 +169,7 @@ public class EnvironmentController : BaseJellyfinApiController
// Check if unc share
var index = path.LastIndexOf(UncSeparator);
- if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
+ if (index != -1 && path[0] == UncSeparator)
{
parent = path.Substring(0, index);
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index d51a5325f..baeb8b81a 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -50,7 +50,7 @@ public class FilterController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index da60f2c60..6cb1993e4 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -85,7 +85,7 @@ public class GenresController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
@@ -131,8 +131,8 @@ public class GenresController : BaseJellyfinApiController
QueryResult<(BaseItem, ItemCounts)> result;
if (parentItem is ICollectionFolder parentCollectionFolder
- && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
- || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
+ && (parentCollectionFolder.CollectionType == CollectionType.music
+ || parentCollectionFolder.CollectionType == CollectionType.musicvideos))
{
result = _libraryManager.GetMusicGenres(query);
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 6eedfd8c7..392d9955f 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -160,7 +160,7 @@ public class HlsSegmentController : BaseJellyfinApiController
var pathExtension = Path.GetExtension(path);
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
- && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
+ && path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase))
{
playlistPath = path;
break;
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 7b10ea170..c031ce338 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -13,6 +13,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
@@ -79,7 +80,7 @@ public class ImageController : BaseJellyfinApiController
_appPaths = appPaths;
}
- private static Stream GetFromBase64Stream(Stream inputStream)
+ private static CryptoStream GetFromBase64Stream(Stream inputStream)
=> new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
/// <summary>
@@ -2079,30 +2080,30 @@ public class ImageController : BaseJellyfinApiController
foreach (var (key, value) in headers)
{
- Response.Headers.Add(key, value);
+ Response.Headers.Append(key, value);
}
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);
+ Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
+ Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
if (disableCaching)
{
- Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
- Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
+ Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
+ Response.Headers.Append(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
}
else
{
if (cacheDuration.HasValue)
{
- Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
+ Response.Headers.Append(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
}
else
{
- Response.Headers.Add(HeaderNames.CacheControl, "public");
+ Response.Headers.Append(HeaderNames.CacheControl, "public");
}
- Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
+ Response.Headers.Append(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
// if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index b030e74dd..e3aee1bf7 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index b8f6e91ad..0a8522e1c 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 504f2fa1d..9800248c6 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -5,6 +5,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -164,18 +166,16 @@ public class ItemUpdateController : BaseJellyfinApiController
var inheritedContentType = _libraryManager.GetInheritedContentType(item);
var configuredContentType = _libraryManager.GetConfiguredContentType(item);
- if (string.IsNullOrWhiteSpace(inheritedContentType) ||
- !string.IsNullOrWhiteSpace(configuredContentType))
+ if (inheritedContentType is null || configuredContentType is not null)
{
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
info.ContentType = configuredContentType;
- if (string.IsNullOrWhiteSpace(inheritedContentType)
- || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows)
{
info.ContentTypeOptions = info.ContentTypeOptions
.Where(i => string.IsNullOrWhiteSpace(i.Value)
- || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase))
.ToArray();
}
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 80128536d..a1fc8e11b 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -34,6 +34,7 @@ public class ItemsController : BaseJellyfinApiController
private readonly IDtoService _dtoService;
private readonly ILogger<ItemsController> _logger;
private readonly ISessionManager _sessionManager;
+ private readonly IUserDataManager _userDataRepository;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsController"/> class.
@@ -44,13 +45,15 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
public ItemsController(
IUserManager userManager,
ILibraryManager libraryManager,
ILocalizationManager localization,
IDtoService dtoService,
ILogger<ItemsController> logger,
- ISessionManager sessionManager)
+ ISessionManager sessionManager,
+ IUserDataManager userDataRepository)
{
_userManager = userManager;
_libraryManager = libraryManager;
@@ -58,6 +61,7 @@ public class ItemsController : BaseJellyfinApiController
_dtoService = dtoService;
_logger = logger;
_sessionManager = sessionManager;
+ _userDataRepository = userDataRepository;
}
/// <summary>
@@ -195,9 +199,9 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -269,13 +273,13 @@ public class ItemsController : BaseJellyfinApiController
folder = _libraryManager.GetUserRootFolder();
}
- string? collectionType = null;
+ CollectionType? collectionType = null;
if (folder is IHasCollectionType hasCollectionType)
{
collectionType = hasCollectionType.CollectionType;
}
- if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.playlists)
{
recursive = true;
includeItemTypes = new[] { BaseItemKind.Playlist };
@@ -652,9 +656,9 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -812,7 +816,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -881,4 +885,64 @@ public class ItemsController : BaseJellyfinApiController
itemsResult.TotalRecordCount,
returnItems);
}
+
+ /// <summary>
+ /// Get Item User Data.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="200">return item user data.</response>
+ /// <response code="404">Item is not found.</response>
+ /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
+ [HttpGet("Users/{userId}/Items/{itemId}/UserData")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<UserItemDataDto> GetItemUserData(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data.");
+ }
+
+ var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
+ var item = _libraryManager.GetItemById(itemId);
+
+ return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user);
+ }
+
+ /// <summary>
+ /// Update Item User Data.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userDataDto">New user data object.</param>
+ /// <response code="200">return updated user item data.</response>
+ /// <response code="404">Item is not found.</response>
+ /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
+ [HttpPost("Users/{userId}/Items/{itemId}/UserData")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<UserItemDataDto> UpdateItemUserData(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
+ [FromBody, Required] UpdateUserItemDataDto userDataDto)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
+ }
+
+ var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ _userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData);
+
+ return _userDataRepository.GetUserDataDto(item, user);
+ }
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 46c0a8d52..de057bbab 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -15,6 +15,7 @@ using Jellyfin.Api.Models.LibraryDtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Configuration;
@@ -294,8 +295,8 @@ public class LibraryController : BaseJellyfinApiController
return new AllThemeMediaResult
{
- ThemeSongsResult = themeSongs?.Value,
- ThemeVideosResult = themeVideos?.Value,
+ ThemeSongsResult = themeSongs.Value,
+ ThemeVideosResult = themeVideos.Value,
SoundtrackSongsResult = new ThemeMediaResult()
};
}
@@ -490,7 +491,7 @@ public class LibraryController : BaseJellyfinApiController
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
- parent = parent?.GetParent();
+ parent = parent.GetParent();
}
return baseItemDtos;
@@ -788,7 +789,7 @@ public class LibraryController : BaseJellyfinApiController
[Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
- [FromQuery] string? libraryContentType,
+ [FromQuery] CollectionType? libraryContentType,
[FromQuery] bool isNewLibrary = false)
{
var result = new LibraryOptionsResultDto();
@@ -922,19 +923,19 @@ public class LibraryController : BaseJellyfinApiController
}
}
- private static string[] GetRepresentativeItemTypes(string? contentType)
+ private static string[] GetRepresentativeItemTypes(CollectionType? contentType)
{
return contentType switch
{
- CollectionType.BoxSets => new[] { "BoxSet" },
- CollectionType.Playlists => new[] { "Playlist" },
- CollectionType.Movies => new[] { "Movie" },
- CollectionType.TvShows => new[] { "Series", "Season", "Episode" },
- CollectionType.Books => new[] { "Book" },
- CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
- CollectionType.HomeVideos => new[] { "Video", "Photo" },
- CollectionType.Photos => new[] { "Video", "Photo" },
- CollectionType.MusicVideos => new[] { "MusicVideo" },
+ CollectionType.boxsets => new[] { "BoxSet" },
+ CollectionType.playlists => new[] { "Playlist" },
+ CollectionType.movies => new[] { "Movie" },
+ CollectionType.tvshows => new[] { "Series", "Season", "Episode" },
+ CollectionType.books => new[] { "Book" },
+ CollectionType.music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
+ CollectionType.homevideos => new[] { "Video", "Photo" },
+ CollectionType.photos => new[] { "Video", "Photo" },
+ CollectionType.musicvideos => new[] { "MusicVideo" },
_ => new[] { "Series", "Season", "Episode", "Movie" }
};
}
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index b012ff42e..d483ca4d2 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 649397d68..425086895 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -16,6 +16,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
@@ -143,7 +144,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true)
@@ -547,7 +548,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isSports,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
index b9772a069..f65d95c41 100644
--- a/Jellyfin.Api/Controllers/LocalizationController.cs
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Api;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using Microsoft.AspNetCore.Authorization;
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 435457af6..69b904264 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -85,7 +85,7 @@ public class MusicGenresController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
@@ -150,7 +150,7 @@ public class MusicGenresController : BaseJellyfinApiController
MusicGenre? item;
- if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
+ if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
{
item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre);
}
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 0ba5e995f..c5e940108 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates;
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 8d2a738d4..c4c89ccde 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@@ -75,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery, ParameterObsolete] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
[FromQuery, ParameterObsolete] Guid? userId,
- [FromQuery, ParameterObsolete] string? mediaType,
+ [FromQuery, ParameterObsolete] MediaType? mediaType,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
{
if (ids.Count == 0)
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 72ad14a28..f63e63927 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Net;
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 5c77db240..595cab2df 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index c8fa11ac6..065466cbc 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Api;
using MediaBrowser.Model.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 387b3ea5a..5b4594165 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -86,7 +86,7 @@ public class SearchController : BaseJellyfinApiController
[FromQuery, Required] string searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery] Guid? parentId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index e93456de6..fdebb3d45 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -10,6 +10,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.SessionDtos;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
@@ -90,12 +91,6 @@ public class SessionController : BaseJellyfinApiController
result = result.Where(i => !i.UserId.Equals(default));
}
- if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
- {
- var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
- result = result.Where(i => i.LastActivityDate >= minActiveDate);
- }
-
result = result.Where(i =>
{
if (!string.IsNullOrWhiteSpace(i.DeviceId))
@@ -110,6 +105,12 @@ public class SessionController : BaseJellyfinApiController
});
}
+ if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
+ {
+ var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
+ result = result.Where(i => i.LastActivityDate >= minActiveDate);
+ }
+
return Ok(result);
}
@@ -393,7 +394,7 @@ public class SessionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> PostCapabilities(
[FromQuery] string? id,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 1098733b2..41b0858d1 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -3,7 +3,8 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.StartupDtos;
-using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Api;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index fb89e9610..49ca058bd 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -14,6 +14,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.SubtitleDtos;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -115,7 +116,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="200">Subtitles retrieved.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -135,7 +136,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="204">Subtitle downloaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -399,7 +400,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="204">Subtitle uploaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")]
- [Authorize(Policy = Policies.RequiresElevation)]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId,
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 5b808f257..675757fc5 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -56,7 +56,7 @@ public class SuggestionsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromRoute, Required] Guid userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 23abba7dc..383978197 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.SyncPlayDtos;
+using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay;
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 11095a97f..3d4df0386 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index b5b640620..4fbaafa2a 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -160,9 +160,9 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
new file mode 100644
index 000000000..2dc960229
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -0,0 +1,101 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Trickplay controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class TrickplayController : BaseJellyfinApiController
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ITrickplayManager _trickplayManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayController"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param>
+ /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
+ public TrickplayController(
+ ILibraryManager libraryManager,
+ ITrickplayManager trickplayManager)
+ {
+ _libraryManager = libraryManager;
+ _trickplayManager = trickplayManager;
+ }
+
+ /// <summary>
+ /// Gets an image tiles playlist for trickplay.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="width">The width of a single tile.</param>
+ /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
+ /// <response code="200">Tiles playlist returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the trickplay playlist file.</returns>
+ [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetTrickplayHlsPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int width,
+ [FromQuery] Guid? mediaSourceId)
+ {
+ string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(playlist))
+ {
+ return NotFound();
+ }
+
+ return Content(playlist, MimeTypes.GetMimeType("playlist.m3u8"), Encoding.UTF8);
+ }
+
+ /// <summary>
+ /// Gets a trickplay tile image.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="width">The width of a single tile.</param>
+ /// <param name="index">The index of the desired tile.</param>
+ /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
+ /// <response code="200">Tile image returned.</response>
+ /// <response code="200">Tile image not found at specified index.</response>
+ /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns>
+ [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public ActionResult GetTrickplayTileImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int width,
+ [FromRoute, Required] int index,
+ [FromQuery] Guid? mediaSourceId)
+ {
+ var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
+ if (System.IO.File.Exists(path))
+ {
+ return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
+ }
+
+ return NotFound();
+ }
+}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index bdbbd1e0d..55a30d469 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -135,7 +135,7 @@ public class TvShowsController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the upcoming episodes.</returns>
[HttpGet("Upcoming")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
@@ -219,7 +219,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
- [FromQuery] string? sortBy)
+ [FromQuery] ItemSortBy? sortBy)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
@@ -289,7 +289,7 @@ public class TvShowsController : BaseJellyfinApiController
episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
}
- if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
+ if (sortBy == ItemSortBy.Random)
{
episodes.Shuffle();
}
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 1be40111d..f9f27f148 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.UserDtos;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 838b43234..0ffa3ab1a 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -6,6 +6,7 @@ using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -63,7 +64,7 @@ public class UserViewsController : BaseJellyfinApiController
public QueryResult<BaseItemDto> GetUserViews(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false)
{
var query = new UserViewQuery
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index c0ec646ed..5d9868eb9 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -12,11 +12,10 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -41,11 +40,9 @@ public class VideosController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
- private readonly IDlnaManager _dlnaManager;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
private readonly EncodingHelper _encodingHelper;
@@ -58,11 +55,9 @@ public class VideosController : BaseJellyfinApiController
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
@@ -70,11 +65,9 @@ public class VideosController : BaseJellyfinApiController
ILibraryManager libraryManager,
IUserManager userManager,
IDtoService dtoService,
- IDlnaManager dlnaManager,
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
IHttpClientFactory httpClientFactory,
EncodingHelper encodingHelper)
@@ -82,11 +75,9 @@ public class VideosController : BaseJellyfinApiController
_libraryManager = libraryManager;
_userManager = userManager;
_dtoService = dtoService;
- _dlnaManager = dlnaManager;
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
_encodingHelper = encodingHelper;
@@ -323,7 +314,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
+ [FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
@@ -380,7 +371,6 @@ public class VideosController : BaseJellyfinApiController
Static = @static ?? false,
Params = @params,
Tag = tag,
- DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
@@ -437,8 +427,6 @@ public class VideosController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _dlnaManager,
- _deviceManager,
_transcodingJobHelper,
_transcodingJobType,
cancellationTokenSource.Token)
@@ -446,8 +434,6 @@ public class VideosController : BaseJellyfinApiController
if (@static.HasValue && @static.Value && state.DirectStreamProvider is not null)
{
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
-
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
if (liveStreamInfo is null)
{
@@ -462,8 +448,6 @@ public class VideosController : BaseJellyfinApiController
// Static remote stream
if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
{
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
-
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false);
}
@@ -474,12 +458,6 @@ public class VideosController : BaseJellyfinApiController
}
var outputPath = state.OutputFilePath;
- var outputPathExists = System.IO.File.Exists(outputPath);
-
- var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
- var isTranscodeCached = outputPathExists && transcodingJob is not null;
-
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager);
// Static stream
if (@static.HasValue && @static.Value)
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 74370db50..ca46c38c5 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -76,8 +76,8 @@ public class YearsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -191,7 +191,7 @@ public class YearsController : BaseJellyfinApiController
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
- private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<string> mediaTypes)
+ private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<MediaType> mediaTypes)
{
var baseItemKind = f.GetBaseItemKind();
// Exclude item types
@@ -207,7 +207,7 @@ public class YearsController : BaseJellyfinApiController
}
// Include MediaTypes
- if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType))
{
return false;
}
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index 2d7a56d91..7d9823c25 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -38,10 +38,10 @@ public static class DtoExtensions
if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
{
- if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
+ if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("wmc", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("classic", StringComparison.OrdinalIgnoreCase))
{
int oldLen = dtoOptions.Fields.Count;
var arr = new ItemFields[oldLen + 1];
@@ -53,13 +53,13 @@ public static class DtoExtensions
if (!dtoOptions.ContainsField(ItemFields.ChildCount))
{
- if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
- client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
+ if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("wmc", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("classic", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("roku", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("samsung", StringComparison.OrdinalIgnoreCase) ||
+ client.Contains("androidtv", StringComparison.OrdinalIgnoreCase))
{
int oldLen = dtoOptions.Fields.Count;
var arr = new ItemFields[oldLen + 1];
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index 2b18c389d..926ce99dd 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -7,8 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.MediaInfo;
@@ -23,13 +21,11 @@ namespace Jellyfin.Api.Helpers;
/// </summary>
public class AudioHelper
{
- private readonly IDlnaManager _dlnaManager;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
@@ -38,37 +34,31 @@ public class AudioHelper
/// <summary>
/// Initializes a new instance of the <see cref="AudioHelper"/> class.
/// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
public AudioHelper(
- IDlnaManager dlnaManager,
IUserManager userManager,
ILibraryManager libraryManager,
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor,
EncodingHelper encodingHelper)
{
- _dlnaManager = dlnaManager;
_userManager = userManager;
_libraryManager = libraryManager;
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor;
@@ -104,8 +94,6 @@ public class AudioHelper
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _dlnaManager,
- _deviceManager,
_transcodingJobHelper,
transcodingJobType,
cancellationTokenSource.Token)
@@ -113,8 +101,6 @@ public class AudioHelper
if (streamingRequest.Static && state.DirectStreamProvider is not null)
{
- StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
-
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
if (liveStreamInfo is null)
{
@@ -129,8 +115,6 @@ public class AudioHelper
// Static remote stream
if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http)
{
- StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
-
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false);
}
@@ -141,12 +125,6 @@ public class AudioHelper
}
var outputPath = state.OutputFilePath;
- var outputPathExists = File.Exists(outputPath);
-
- var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
- var isTranscodeCached = outputPathExists && transcodingJob is not null;
-
- StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
// Static stream
if (streamingRequest.Static)
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 276a09f41..05f7d44bf 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -9,16 +9,16 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Net;
@@ -36,58 +36,54 @@ public class DynamicHlsHelper
{
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly INetworkManager _networkManager;
private readonly ILogger<DynamicHlsHelper> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly EncodingHelper _encodingHelper;
+ private readonly ITrickplayManager _trickplayManager;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
public DynamicHlsHelper(
ILibraryManager libraryManager,
IUserManager userManager,
- IDlnaManager dlnaManager,
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
INetworkManager networkManager,
ILogger<DynamicHlsHelper> logger,
IHttpContextAccessor httpContextAccessor,
- EncodingHelper encodingHelper)
+ EncodingHelper encodingHelper,
+ ITrickplayManager trickplayManager)
{
_libraryManager = libraryManager;
_userManager = userManager;
- _dlnaManager = dlnaManager;
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_networkManager = networkManager;
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_encodingHelper = encodingHelper;
+ _trickplayManager = trickplayManager;
}
/// <summary>
@@ -134,14 +130,12 @@ public class DynamicHlsHelper
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
- _dlnaManager,
- _deviceManager,
_transcodingJobHelper,
transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
- _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0");
+ _httpContextAccessor.HttpContext.Response.Headers.Append(HeaderNames.Expires, "0");
if (isHeadRequest)
{
return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
@@ -280,6 +274,13 @@ public class DynamicHlsHelper
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
+ if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
+ {
+ var sourceId = Guid.Parse(state.Request.MediaSourceId);
+ var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
+ AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
+ }
+
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
@@ -509,6 +510,41 @@ public class DynamicHlsHelper
}
/// <summary>
+ /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
+ /// </summary>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="user">Http user context.</param>
+ private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder builder, ClaimsPrincipal user)
+ {
+ const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\"";
+
+ foreach (var resolution in trickplayResolutions)
+ {
+ var width = resolution.Key;
+ var trickplayInfo = resolution.Value;
+
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
+ width.ToString(CultureInfo.InvariantCulture),
+ state.Request.MediaSourceId,
+ user.GetToken());
+
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ playlistFormat,
+ trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
+ url);
+
+ builder.AppendLine();
+ }
+ }
+
+ /// <summary>
/// Get the H.26X level of the output video stream.
/// </summary>
/// <param name="state">StreamState of the current stream.</param>
@@ -520,7 +556,7 @@ public class DynamicHlsHelper
&& state.VideoStream is not null
&& state.VideoStream.Level.HasValue)
{
- levelString = state.VideoStream.Level.ToString() ?? string.Empty;
+ levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
}
else
{
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index 2155e305d..e2d3bfb19 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -53,7 +53,7 @@ public static class HlsHelpers
break;
}
- if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+ if (line.Contains("#EXTINF:", StringComparison.OrdinalIgnoreCase))
{
count++;
if (count >= segmentCount)
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index a36028cfe..321987ca7 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -243,7 +243,7 @@ public class MediaInfoHelper
}
// Beginning of Playback Determination
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ var streamInfo = item.MediaType == MediaType.Audio
? streamBuilder.GetOptimalAudioStream(options)
: streamBuilder.GetOptimalVideoStream(options);
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index bc12ca388..be3d4dfb6 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -30,14 +30,14 @@ public static class RequestHelpers
/// <param name="sortBy">Sort By. Comma delimited string.</param>
/// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
/// <returns>Order By.</returns>
- public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
+ public static (ItemSortBy, SortOrder)[] GetOrderBy(IReadOnlyList<ItemSortBy> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
{
if (sortBy.Count == 0)
{
- return Array.Empty<(string, SortOrder)>();
+ return Array.Empty<(ItemSortBy, SortOrder)>();
}
- var result = new (string, SortOrder)[sortBy.Count];
+ var result = new (ItemSortBy, SortOrder)[sortBy.Count];
var i = 0;
// Add elements which have a SortOrder specified
for (; i < requestedSortOrder.Count; i++)
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 11f6bcf6b..71c62b235 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -7,19 +7,17 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Helpers;
@@ -40,8 +38,6 @@ public static class StreamingHelpers
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
@@ -55,21 +51,11 @@ public static class StreamingHelpers
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
EncodingHelper encodingHelper,
- IDlnaManager dlnaManager,
- IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
TranscodingJobType transcodingJobType,
CancellationToken cancellationToken)
{
var httpRequest = httpContext.Request;
- // Parse the DLNA time seek header
- if (!streamingRequest.StartTimeTicks.HasValue)
- {
- var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];
-
- streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());
- }
-
if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
{
ParseParams(streamingRequest);
@@ -88,16 +74,11 @@ public static class StreamingHelpers
streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
}
- var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
- streamingRequest.StreamOptions.ContainsKey("dlnaheaders") ||
- string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
-
var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
{
Request = streamingRequest,
RequestedUrl = url,
- UserAgent = httpRequest.Headers[HeaderNames.UserAgent],
- EnableDlnaHeaders = enableDlnaHeaders
+ UserAgent = httpRequest.Headers[HeaderNames.UserAgent]
};
var userId = httpContext.User.GetUserId();
@@ -128,7 +109,7 @@ public static class StreamingHelpers
var item = libraryManager.GetItemById(streamingRequest.Id);
- state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+ state.IsInputVideo = item.MediaType == MediaType.Video;
MediaSourceInfo? mediaSource = null;
if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
@@ -242,8 +223,6 @@ public static class StreamingHelpers
}
}
- ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
-
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
? GetOutputFileExtension(state, mediaSource)
: ("." + state.OutputContainer);
@@ -254,123 +233,6 @@ public static class StreamingHelpers
}
/// <summary>
- /// Adds the dlna headers.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- /// <param name="request">The <see cref="HttpRequest"/>.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public static void AddDlnaHeaders(
- StreamState state,
- IHeaderDictionary responseHeaders,
- bool isStaticallyStreamed,
- long? startTimeTicks,
- HttpRequest request,
- IDlnaManager dlnaManager)
- {
- if (!state.EnableDlnaHeaders)
- {
- return;
- }
-
- var profile = state.DeviceProfile;
-
- StringValues transferMode = request.Headers["transferMode.dlna.org"];
- responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString());
- responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*");
-
- if (state.RunTimeTicks.HasValue)
- {
- if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase))
- {
- var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
- responseHeaders.Add("MediaInfo.sec", string.Format(
- CultureInfo.InvariantCulture,
- "SEC_Duration={0};",
- Convert.ToInt32(ms)));
- }
-
- if (!isStaticallyStreamed && profile is not null)
- {
- AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
- }
- }
-
- profile ??= dlnaManager.GetDefaultProfile();
-
- var audioCodec = state.ActualOutputAudioCodec;
-
- if (!state.IsVideoRequest)
- {
- responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader(
- profile,
- state.OutputContainer,
- audioCodec,
- state.OutputAudioBitrate,
- state.OutputAudioSampleRate,
- state.OutputAudioChannels,
- state.OutputAudioBitDepth,
- isStaticallyStreamed,
- state.RunTimeTicks,
- state.TranscodeSeekInfo));
- }
- else
- {
- var videoCodec = state.ActualOutputVideoCodec;
-
- responseHeaders.Add(
- "contentFeatures.dlna.org",
- ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
- }
- }
-
- /// <summary>
- /// Parses the time seek header.
- /// </summary>
- /// <param name="value">The time seek header string.</param>
- /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
- private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value)
- {
- if (value.IsEmpty)
- {
- return null;
- }
-
- const string npt = "npt=";
- if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase))
- {
- throw new ArgumentException("Invalid timeseek header");
- }
-
- var index = value.IndexOf('-');
- value = index == -1
- ? value.Slice(npt.Length)
- : value.Slice(npt.Length, index - npt.Length);
- if (!value.Contains(':'))
- {
- // Parses npt times in the format of '417.33'
- if (double.TryParse(value, CultureInfo.InvariantCulture, out var seconds))
- {
- return TimeSpan.FromSeconds(seconds).Ticks;
- }
-
- throw new ArgumentException("Invalid timeseek header");
- }
-
- try
- {
- // Parses npt times in the format of '10:19:25.7'
- return TimeSpan.Parse(value, CultureInfo.InvariantCulture).Ticks;
- }
- catch
- {
- throw new ArgumentException("Invalid timeseek header");
- }
- }
-
- /// <summary>
/// Parses query parameters as StreamOptions.
/// </summary>
/// <param name="queryString">The query string.</param>
@@ -393,29 +255,6 @@ public static class StreamingHelpers
}
/// <summary>
- /// Adds the dlna time seek headers to the response.
- /// </summary>
- /// <param name="state">The current <see cref="StreamState"/>.</param>
- /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
- {
- var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
- var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
-
- responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
- CultureInfo.InvariantCulture,
- "npt={0}-{1}/{1}",
- startSeconds,
- runtimeSeconds));
- responseHeaders.Add("X-AvailableSeekRange", string.Format(
- CultureInfo.InvariantCulture,
- "1 npt={0}-{1}",
- startSeconds,
- runtimeSeconds));
- }
-
- /// <summary>
/// Gets the output file extension.
/// </summary>
/// <param name="state">The state.</param>
@@ -518,79 +357,6 @@ public static class StreamingHelpers
return Path.Combine(folder, filename + ext);
}
- private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
- {
- if (!string.IsNullOrWhiteSpace(deviceProfileId))
- {
- state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
-
- if (state.DeviceProfile is null)
- {
- var caps = deviceManager.GetCapabilities(deviceProfileId);
- state.DeviceProfile = caps is null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
- }
- }
-
- var profile = state.DeviceProfile;
-
- if (profile is null)
- {
- // Don't use settings from the default profile.
- // Only use a specific profile if it was requested.
- return;
- }
-
- var audioCodec = state.ActualOutputAudioCodec;
- var videoCodec = state.ActualOutputVideoCodec;
-
- var mediaProfile = !state.IsVideoRequest
- ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
- : profile.GetVideoMediaProfile(
- state.OutputContainer,
- audioCodec,
- videoCodec,
- state.OutputWidth,
- state.OutputHeight,
- state.TargetVideoBitDepth,
- state.OutputVideoBitrate,
- state.TargetVideoProfile,
- state.TargetVideoRangeType,
- state.TargetVideoLevel,
- state.TargetFramerate,
- state.TargetPacketLength,
- state.TargetTimestamp,
- state.IsTargetAnamorphic,
- state.IsTargetInterlaced,
- state.TargetRefFrames,
- state.TargetVideoStreamCount,
- state.TargetAudioStreamCount,
- state.TargetVideoCodecTag,
- state.IsTargetAVC);
-
- if (mediaProfile is not null)
- {
- state.MimeType = mediaProfile.MimeType;
- }
-
- if (!(@static.HasValue && @static.Value))
- {
- var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
-
- if (transcodingProfile is not null)
- {
- state.EstimateContentLength = transcodingProfile.EstimateContentLength;
- // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
- state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
-
- if (state.VideoRequest is not null)
- {
- state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
- state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
- }
- }
- }
- }
-
/// <summary>
/// Parses the parameters.
/// </summary>
@@ -618,7 +384,7 @@ public static class StreamingHelpers
switch (i)
{
case 0:
- request.DeviceProfileId = val;
+ // DeviceProfileId
break;
case 1:
request.DeviceId = val;
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index c16a586d6..77d3edbd6 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -280,6 +280,7 @@ public class TranscodingJobHelper : IDisposable
if (job.CancellationTokenSource?.IsCancellationRequested == false)
{
+#pragma warning disable CA1849 // Can't await in lock block
job.CancellationTokenSource.Cancel();
}
}
@@ -291,7 +292,6 @@ public class TranscodingJobHelper : IDisposable
lock (job.ProcessLock!)
{
-#pragma warning disable CA1849 // Can't await in lock block
job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
var process = job.Process;
@@ -405,7 +405,7 @@ public class TranscodingJobHelper : IDisposable
var name = Path.GetFileNameWithoutExtension(outputFilePath);
var filesToDelete = _fileSystem.GetFilePaths(directory)
- .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
+ .Where(f => f.Contains(name, StringComparison.OrdinalIgnoreCase));
List<Exception>? exs = null;
foreach (var file in filesToDelete)
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 7ac231885..5f86a6b6b 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -18,14 +18,18 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
+ <ProjectReference Include="..\src\Jellyfin.Networking\Jellyfin.Networking.csproj" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs
index 2241c68e7..cbd948db0 100644
--- a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
-using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
diff --git a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
index 060c14f89..acbb4877d 100644
--- a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
@@ -122,17 +122,17 @@ public class ExceptionMiddleware
private static int GetStatusCode(Exception ex)
{
- switch (ex)
+ return ex switch
{
- case ArgumentException _: return StatusCodes.Status400BadRequest;
- case AuthenticationException _: return StatusCodes.Status401Unauthorized;
- case SecurityException _: return StatusCodes.Status403Forbidden;
- case DirectoryNotFoundException _:
- case FileNotFoundException _:
- case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
- case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
- default: return StatusCodes.Status500InternalServerError;
- }
+ ArgumentException => StatusCodes.Status400BadRequest,
+ AuthenticationException => StatusCodes.Status401Unauthorized,
+ SecurityException => StatusCodes.Status403Forbidden,
+ DirectoryNotFoundException => StatusCodes.Status404NotFound,
+ FileNotFoundException => StatusCodes.Status404NotFound,
+ ResourceNotFoundException => StatusCodes.Status404NotFound,
+ MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed,
+ _ => StatusCodes.Status500InternalServerError
+ };
}
private string NormalizeExceptionMessage(string msg)
diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
index 94de30d1b..d8c95ddff 100644
--- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
+++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
@@ -1,5 +1,4 @@
using System.Threading.Tasks;
-using Jellyfin.Networking.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 5e7dd689e..6a30de5e6 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -107,7 +107,7 @@ public class GetProgramsDto
/// Optional.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<string> SortBy { get; set; } = Array.Empty<string>();
+ public IReadOnlyList<ItemSortBy> SortBy { get; set; } = Array.Empty<ItemSortBy>();
/// <summary>
/// Gets or sets sort Order - Ascending,Descending.
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index 1fba32c5b..bdc488871 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
namespace Jellyfin.Api.Models.PlaylistDtos;
@@ -29,5 +30,5 @@ public class CreatePlaylistDto
/// <summary>
/// Gets or sets the media type.
/// </summary>
- public string? MediaType { get; set; }
+ public MediaType? MediaType { get; set; }
}
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
index b88be33b2..b021771a0 100644
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Session;
@@ -16,7 +17,7 @@ public class ClientCapabilitiesDto
/// Gets or sets the list of playable media types.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<string> PlayableMediaTypes { get; set; } = Array.Empty<string>();
+ public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = Array.Empty<MediaType>();
/// <summary>
/// Gets or sets the list of supported commands.
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index b75272d3f..cc1f9163e 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -86,11 +86,11 @@ public class StreamState : EncodingJobInfo, IDisposable
{
var userAgent = UserAgent ?? string.Empty;
- if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1
- || userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1
- || userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1
- || userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1
- || userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
+ if (userAgent.Contains("AppleTV", StringComparison.OrdinalIgnoreCase)
+ || userAgent.Contains("cfnetwork", StringComparison.OrdinalIgnoreCase)
+ || userAgent.Contains("ipad", StringComparison.OrdinalIgnoreCase)
+ || userAgent.Contains("iphone", StringComparison.OrdinalIgnoreCase)
+ || userAgent.Contains("ipod", StringComparison.OrdinalIgnoreCase))
{
return 6;
}
@@ -139,16 +139,6 @@ public class StreamState : EncodingJobInfo, IDisposable
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether to enable dlna headers.
- /// </summary>
- public bool EnableDlnaHeaders { get; set; }
-
- /// <summary>
- /// Gets or sets the device profile.
- /// </summary>
- public DeviceProfile? DeviceProfile { get; set; }
-
- /// <summary>
/// Gets or sets the transcoding job.
/// </summary>
public TranscodingJobDto? TranscodingJob { get; set; }
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
index 389d6006d..a357498d4 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
@@ -8,11 +8,6 @@ namespace Jellyfin.Api.Models.StreamingDtos;
public class StreamingRequestDto : BaseEncodingJobOptions
{
/// <summary>
- /// Gets or sets the device profile.
- /// </summary>
- public string? DeviceProfileId { get; set; }
-
- /// <summary>
/// Gets or sets the params.
/// </summary>
public string? Params { get; set; }
diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
index 60c529d4a..8548fec1a 100644
--- a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
@@ -1,4 +1,4 @@
-namespace Jellyfin.Api.Models.StreamingDtos;
+namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary>
/// The video request dto.
@@ -15,4 +15,9 @@ public class VideoRequestDto : StreamingRequestDto
/// Gets or sets a value indicating whether to enable subtitles in the manifest.
/// </summary>
public bool EnableSubtitlesInManifest { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable trickplay images.
+ /// </summary>
+ public bool EnableTrickplay { get; set; }
}
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 5b90d65d8..ba228cb00 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -53,7 +53,10 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
/// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _activityManager.EntryCreated -= OnEntryCreated;
+ if (dispose)
+ {
+ _activityManager.EntryCreated -= OnEntryCreated;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index a9df2d671..37c108d5a 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -58,8 +58,11 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn
/// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _taskManager.TaskExecuting -= OnTaskExecuting;
- _taskManager.TaskCompleted -= OnTaskCompleted;
+ if (dispose)
+ {
+ _taskManager.TaskExecuting -= OnTaskExecuting;
+ _taskManager.TaskCompleted -= OnTaskCompleted;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index b403ff46d..3c2b86142 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -57,13 +57,16 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
/// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
- _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
- _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
- _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
- _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
- _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
- _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ if (dispose)
+ {
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs b/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs
new file mode 100644
index 000000000..ff613d9f8
--- /dev/null
+++ b/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Jellyfin.Data.Attributes;
+
+/// <summary>
+/// Attribute to specify that the enum value is to be ignored when generating the openapi spec.
+/// </summary>
+[AttributeUsage(AttributeTargets.Field)]
+public sealed class OpenApiIgnoreEnumAttribute : Attribute
+{
+}
diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs
new file mode 100644
index 000000000..64e7da1b5
--- /dev/null
+++ b/Jellyfin.Data/Entities/TrickplayInfo.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// An entity representing the metadata for a group of trickplay tiles.
+/// </summary>
+public class TrickplayInfo
+{
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [JsonIgnore]
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets width of an individual thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets height of an individual thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets amount of thumbnails per row.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int TileWidth { get; set; }
+
+ /// <summary>
+ /// Gets or sets amount of thumbnails per column.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int TileHeight { get; set; }
+
+ /// <summary>
+ /// Gets or sets total amount of non-black thumbnails.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int ThumbnailCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets interval in milliseconds between each trickplay thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Interval { get; set; }
+
+ /// <summary>
+ /// Gets or sets peak bandwith usage in bits per second.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Bandwidth { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 58ddaaf83..ea0de3016 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities
/// </summary>
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
+ /// <summary>
+ /// Gets or sets the cast receiver id.
+ /// </summary>
+ [StringLength(32)]
+ public string? CastReceiverId { get; set; }
+
/// <inheritdoc />
[ConcurrencyCheck]
public uint RowVersion { get; private set; }
@@ -499,6 +505,7 @@ namespace Jellyfin.Data.Entities
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
+ Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false));
}
/// <summary>
diff --git a/Jellyfin.Data/Enums/CollectionType.cs b/Jellyfin.Data/Enums/CollectionType.cs
new file mode 100644
index 000000000..e3d3b07af
--- /dev/null
+++ b/Jellyfin.Data/Enums/CollectionType.cs
@@ -0,0 +1,165 @@
+#pragma warning disable SA1300 // The name of a C# element does not begin with an upper-case letter. - disabled due to legacy requirement.
+using Jellyfin.Data.Attributes;
+
+namespace Jellyfin.Data.Enums;
+
+/// <summary>
+/// Collection type.
+/// </summary>
+public enum CollectionType
+{
+ /// <summary>
+ /// Unknown collection.
+ /// </summary>
+ unknown = 0,
+
+ /// <summary>
+ /// Movies collection.
+ /// </summary>
+ movies = 1,
+
+ /// <summary>
+ /// Tv shows collection.
+ /// </summary>
+ tvshows = 2,
+
+ /// <summary>
+ /// Music collection.
+ /// </summary>
+ music = 3,
+
+ /// <summary>
+ /// Music videos collection.
+ /// </summary>
+ musicvideos = 4,
+
+ /// <summary>
+ /// Trailers collection.
+ /// </summary>
+ trailers = 5,
+
+ /// <summary>
+ /// Home videos collection.
+ /// </summary>
+ homevideos = 6,
+
+ /// <summary>
+ /// Box sets collection.
+ /// </summary>
+ boxsets = 7,
+
+ /// <summary>
+ /// Books collection.
+ /// </summary>
+ books = 8,
+
+ /// <summary>
+ /// Photos collection.
+ /// </summary>
+ photos = 9,
+
+ /// <summary>
+ /// Live tv collection.
+ /// </summary>
+ livetv = 10,
+
+ /// <summary>
+ /// Playlists collection.
+ /// </summary>
+ playlists = 11,
+
+ /// <summary>
+ /// Folders collection.
+ /// </summary>
+ folders = 12,
+
+ /// <summary>
+ /// Tv show series collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ tvshowseries = 101,
+
+ /// <summary>
+ /// Tv genres collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ tvgenres = 102,
+
+ /// <summary>
+ /// Tv genre collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ tvgenre = 103,
+
+ /// <summary>
+ /// Tv latest collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ tvlatest = 104,
+
+ /// <summary>
+ /// Tv next up collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ tvnextup = 105,
+
+ /// <summary>
+ /// Tv resume collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ tvresume = 106,
+
+ /// <summary>
+ /// Tv favorite series collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ tvfavoriteseries = 107,
+
+ /// <summary>
+ /// Tv favorite episodes collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ tvfavoriteepisodes = 108,
+
+ /// <summary>
+ /// Latest movies collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ movielatest = 109,
+
+ /// <summary>
+ /// Movies to resume collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ movieresume = 110,
+
+ /// <summary>
+ /// Movie movie collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ moviemovies = 111,
+
+ /// <summary>
+ /// Movie collections collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ moviecollection = 112,
+
+ /// <summary>
+ /// Movie favorites collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ moviefavorites = 113,
+
+ /// <summary>
+ /// Movie genres collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ moviegenres = 114,
+
+ /// <summary>
+ /// Movie genre collection.
+ /// </summary>
+ [OpenApiIgnoreEnum]
+ moviegenre = 115
+}
diff --git a/Jellyfin.Data/Enums/ItemSortBy.cs b/Jellyfin.Data/Enums/ItemSortBy.cs
new file mode 100644
index 000000000..17bf1166d
--- /dev/null
+++ b/Jellyfin.Data/Enums/ItemSortBy.cs
@@ -0,0 +1,167 @@
+namespace Jellyfin.Data.Enums;
+
+/// <summary>
+/// These represent sort orders.
+/// </summary>
+public enum ItemSortBy
+{
+ /// <summary>
+ /// Default sort order.
+ /// </summary>
+ Default = 0,
+
+ /// <summary>
+ /// The aired episode order.
+ /// </summary>
+ AiredEpisodeOrder = 1,
+
+ /// <summary>
+ /// The album.
+ /// </summary>
+ Album = 2,
+
+ /// <summary>
+ /// The album artist.
+ /// </summary>
+ AlbumArtist = 3,
+
+ /// <summary>
+ /// The artist.
+ /// </summary>
+ Artist = 4,
+
+ /// <summary>
+ /// The date created.
+ /// </summary>
+ DateCreated = 5,
+
+ /// <summary>
+ /// The official rating.
+ /// </summary>
+ OfficialRating = 6,
+
+ /// <summary>
+ /// The date played.
+ /// </summary>
+ DatePlayed = 7,
+
+ /// <summary>
+ /// The premiere date.
+ /// </summary>
+ PremiereDate = 8,
+
+ /// <summary>
+ /// The start date.
+ /// </summary>
+ StartDate = 9,
+
+ /// <summary>
+ /// The sort name.
+ /// </summary>
+ SortName = 10,
+
+ /// <summary>
+ /// The name.
+ /// </summary>
+ Name = 11,
+
+ /// <summary>
+ /// The random.
+ /// </summary>
+ Random = 12,
+
+ /// <summary>
+ /// The runtime.
+ /// </summary>
+ Runtime = 13,
+
+ /// <summary>
+ /// The community rating.
+ /// </summary>
+ CommunityRating = 14,
+
+ /// <summary>
+ /// The production year.
+ /// </summary>
+ ProductionYear = 15,
+
+ /// <summary>
+ /// The play count.
+ /// </summary>
+ PlayCount = 16,
+
+ /// <summary>
+ /// The critic rating.
+ /// </summary>
+ CriticRating = 17,
+
+ /// <summary>
+ /// The IsFolder boolean.
+ /// </summary>
+ IsFolder = 18,
+
+ /// <summary>
+ /// The IsUnplayed boolean.
+ /// </summary>
+ IsUnplayed = 19,
+
+ /// <summary>
+ /// The IsPlayed boolean.
+ /// </summary>
+ IsPlayed = 20,
+
+ /// <summary>
+ /// The series sort.
+ /// </summary>
+ SeriesSortName = 21,
+
+ /// <summary>
+ /// The video bitrate.
+ /// </summary>
+ VideoBitRate = 22,
+
+ /// <summary>
+ /// The air time.
+ /// </summary>
+ AirTime = 23,
+
+ /// <summary>
+ /// The studio.
+ /// </summary>
+ Studio = 24,
+
+ /// <summary>
+ /// The IsFavouriteOrLiked boolean.
+ /// </summary>
+ IsFavoriteOrLiked = 25,
+
+ /// <summary>
+ /// The last content added date.
+ /// </summary>
+ DateLastContentAdded = 26,
+
+ /// <summary>
+ /// The series last played date.
+ /// </summary>
+ SeriesDatePlayed = 27,
+
+ /// <summary>
+ /// The parent index number.
+ /// </summary>
+ ParentIndexNumber = 28,
+
+ /// <summary>
+ /// The index number.
+ /// </summary>
+ IndexNumber = 29,
+
+ /// <summary>
+ /// The similarity score.
+ /// </summary>
+ SimilarityScore = 30,
+
+ /// <summary>
+ /// The search score.
+ /// </summary>
+ SearchScore = 31,
+}
diff --git a/Jellyfin.Data/Enums/MediaType.cs b/Jellyfin.Data/Enums/MediaType.cs
new file mode 100644
index 000000000..b014ff366
--- /dev/null
+++ b/Jellyfin.Data/Enums/MediaType.cs
@@ -0,0 +1,32 @@
+namespace Jellyfin.Data.Enums;
+
+/// <summary>
+/// Media types.
+/// </summary>
+public enum MediaType
+{
+ /// <summary>
+ /// Unknown media type.
+ /// </summary>
+ Unknown = 0,
+
+ /// <summary>
+ /// Video media.
+ /// </summary>
+ Video = 1,
+
+ /// <summary>
+ /// Audio media.
+ /// </summary>
+ Audio = 2,
+
+ /// <summary>
+ /// Photo media.
+ /// </summary>
+ Photo = 3,
+
+ /// <summary>
+ /// Book media.
+ /// </summary>
+ Book = 4
+}
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
index 40280b95e..6644f0151 100644
--- a/Jellyfin.Data/Enums/PermissionKind.cs
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -113,6 +113,11 @@ namespace Jellyfin.Data.Enums
/// <summary>
/// Whether the user can create, modify and delete collections.
/// </summary>
- EnableCollectionManagement = 21
+ EnableCollectionManagement = 21,
+
+ /// <summary>
+ /// Whether the user can edit subtitles.
+ /// </summary>
+ EnableSubtitleManagement = 22
}
}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 1bc5d8bf9..75912abf0 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -23,12 +23,12 @@
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
- <ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
- </ItemGroup>
-
- <!-- Code analysers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
deleted file mode 100644
index 90ebcd390..000000000
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-#pragma warning disable CA1819 // Properties should not return arrays
-
-using System;
-
-namespace Jellyfin.Networking.Configuration
-{
- /// <summary>
- /// Defines the <see cref="NetworkConfiguration" />.
- /// </summary>
- public class NetworkConfiguration
- {
- /// <summary>
- /// The default value for <see cref="InternalHttpPort"/>.
- /// </summary>
- public const int DefaultHttpPort = 8096;
-
- /// <summary>
- /// The default value for <see cref="PublicHttpsPort"/> and <see cref="InternalHttpsPort"/>.
- /// </summary>
- public const int DefaultHttpsPort = 8920;
-
- private string _baseUrl = string.Empty;
-
- /// <summary>
- /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
- /// </summary>
- public string BaseUrl
- {
- get => _baseUrl;
- set
- {
- // Normalize the start of the string
- if (string.IsNullOrWhiteSpace(value))
- {
- // If baseUrl is empty, set an empty prefix string
- _baseUrl = string.Empty;
- return;
- }
-
- if (value[0] != '/')
- {
- // If baseUrl was not configured with a leading slash, append one for consistency
- value = "/" + value;
- }
-
- // Normalize the end of the string
- if (value[^1] == '/')
- {
- // If baseUrl was configured with a trailing slash, remove it for consistency
- value = value.Remove(value.Length - 1);
- }
-
- _baseUrl = value;
- }
- }
-
- /// <summary>
- /// Gets or sets a value indicating whether to use HTTPS.
- /// </summary>
- /// <remarks>
- /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
- /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
- /// </remarks>
- public bool EnableHttps { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether the server should force connections over HTTPS.
- /// </summary>
- public bool RequireHttps { get; set; }
-
- /// <summary>
- /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
- /// </summary>
- public string CertificatePath { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
- /// </summary>
- public string CertificatePassword { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the internal HTTP server port.
- /// </summary>
- /// <value>The HTTP server port.</value>
- public int InternalHttpPort { get; set; } = DefaultHttpPort;
-
- /// <summary>
- /// Gets or sets the internal HTTPS server port.
- /// </summary>
- /// <value>The HTTPS server port.</value>
- public int InternalHttpsPort { get; set; } = DefaultHttpsPort;
-
- /// <summary>
- /// Gets or sets the public HTTP port.
- /// </summary>
- /// <value>The public HTTP port.</value>
- public int PublicHttpPort { get; set; } = DefaultHttpPort;
-
- /// <summary>
- /// Gets or sets the public HTTPS port.
- /// </summary>
- /// <value>The public HTTPS port.</value>
- public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
-
- /// <summary>
- /// Gets or sets a value indicating whether Autodiscovery is enabled.
- /// </summary>
- public bool AutoDiscovery { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable automatic port forwarding.
- /// </summary>
- public bool EnableUPnP { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether IPv6 is enabled.
- /// </summary>
- public bool EnableIPv4 { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating whether IPv6 is enabled.
- /// </summary>
- public bool EnableIPv6 { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether access from outside of the LAN is permitted.
- /// </summary>
- public bool EnableRemoteAccess { get; set; } = true;
-
- /// <summary>
- /// Gets or sets the subnets that are deemed to make up the LAN.
- /// </summary>
- public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.
- /// </summary>
- public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets the known proxies.
- /// </summary>
- public string[] KnownProxies { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be ignored for the purposes of binding.
- /// </summary>
- public bool IgnoreVirtualInterfaces { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. <seealso cref="IgnoreVirtualInterfaces"/>.
- /// </summary>
- public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" };
-
- /// <summary>
- /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests.
- /// </summary>
- public bool EnablePublishedServerUriByRequest { get; set; } = false;
-
- /// <summary>
- /// Gets or sets the PublishedServerUriBySubnet
- /// Gets or sets PublishedServerUri to advertise for specific subnets.
- /// </summary>
- public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>.
- /// </summary>
- public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist.
- /// </summary>
- public bool IsRemoteIPFilterBlacklist { get; set; }
- }
-}
diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs
deleted file mode 100644
index 3ba6bb8fc..000000000
--- a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MediaBrowser.Common.Configuration;
-
-namespace Jellyfin.Networking.Configuration
-{
- /// <summary>
- /// Defines the <see cref="NetworkConfigurationExtensions" />.
- /// </summary>
- public static class NetworkConfigurationExtensions
- {
- /// <summary>
- /// Retrieves the network configuration.
- /// </summary>
- /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
- /// <returns>The <see cref="NetworkConfiguration"/>.</returns>
- public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config)
- {
- return config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
- }
- }
-}
diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs
deleted file mode 100644
index 14726565a..000000000
--- a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Collections.Generic;
-using MediaBrowser.Common.Configuration;
-
-namespace Jellyfin.Networking.Configuration
-{
- /// <summary>
- /// Defines the <see cref="NetworkConfigurationFactory" />.
- /// </summary>
- public class NetworkConfigurationFactory : IConfigurationFactory
- {
- /// <summary>
- /// The GetConfigurations.
- /// </summary>
- /// <returns>The <see cref="IEnumerable{ConfigurationStore}"/>.</returns>
- public IEnumerable<ConfigurationStore> GetConfigurations()
- {
- return new[]
- {
- new NetworkConfigurationStore()
- };
- }
- }
-}
diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs
deleted file mode 100644
index a268ebb68..000000000
--- a/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using MediaBrowser.Common.Configuration;
-
-namespace Jellyfin.Networking.Configuration
-{
- /// <summary>
- /// A configuration that stores network related settings.
- /// </summary>
- public class NetworkConfigurationStore : ConfigurationStore
- {
- /// <summary>
- /// The name of the configuration in the storage.
- /// </summary>
- public const string StoreKey = "network";
-
- /// <summary>
- /// Initializes a new instance of the <see cref="NetworkConfigurationStore"/> class.
- /// </summary>
- public NetworkConfigurationStore()
- {
- ConfigurationType = typeof(NetworkConfiguration);
- Key = StoreKey;
- }
- }
-}
diff --git a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs
deleted file mode 100644
index 59e6956c7..000000000
--- a/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
-The MIT License (MIT)
-
-Copyright (c) .NET Foundation and Contributors
-
-All rights reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-using System.IO;
-using System.Net.Http;
-using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Jellyfin.Networking.HappyEyeballs
-{
- /// <summary>
- /// Defines the <see cref="HttpClientExtension"/> class.
- ///
- /// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 .
- /// </summary>
- public static class HttpClientExtension
- {
- /// <summary>
- /// Gets or sets a value indicating whether the client should use IPv6.
- /// </summary>
- public static bool UseIPv6 { get; set; } = true;
-
- /// <summary>
- /// Implements the httpclient callback method.
- /// </summary>
- /// <param name="context">The <see cref="SocketsHttpConnectionContext"/> instance.</param>
- /// <param name="cancellationToken">The <see cref="CancellationToken"/> instance.</param>
- /// <returns>The http steam.</returns>
- public static async ValueTask<Stream> OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
- {
- if (!UseIPv6)
- {
- return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false);
- }
-
- using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token);
-
- // GetAwaiter().GetResult() is used instead of .Result as this results in improved exception handling.
- // The tasks have already been completed.
- // See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details.
- if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully)
- {
- cancelIPv6.Cancel();
- return tryConnectAsyncIPv6.GetAwaiter().GetResult();
- }
-
- using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token);
-
- if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6)
- {
- if (tryConnectAsyncIPv6.IsCompletedSuccessfully)
- {
- cancelIPv4.Cancel();
- return tryConnectAsyncIPv6.GetAwaiter().GetResult();
- }
-
- return tryConnectAsyncIPv4.GetAwaiter().GetResult();
- }
- else
- {
- if (tryConnectAsyncIPv4.IsCompletedSuccessfully)
- {
- cancelIPv6.Cancel();
- return tryConnectAsyncIPv4.GetAwaiter().GetResult();
- }
-
- return tryConnectAsyncIPv6.GetAwaiter().GetResult();
- }
- }
-
- private static async Task<Stream> AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
- {
- // The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
- var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
- {
- // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
- NoDelay = true
- };
-
- try
- {
- await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
- // The stream should take the ownership of the underlying socket,
- // closing it when it's disposed.
- return new NetworkStream(socket, ownsSocket: true);
- }
- catch
- {
- socket.Dispose();
- throw;
- }
- }
- }
-}
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
deleted file mode 100644
index 9c59500d7..000000000
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ /dev/null
@@ -1,1127 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Net.Sockets;
-using System.Threading;
-using Jellyfin.Networking.Configuration;
-using Jellyfin.Networking.Constants;
-using Jellyfin.Networking.Extensions;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.Net;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.HttpOverrides;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
-
-namespace Jellyfin.Networking.Manager
-{
- /// <summary>
- /// Class to take care of network interface management.
- /// </summary>
- public class NetworkManager : INetworkManager, IDisposable
- {
- /// <summary>
- /// Threading lock for network properties.
- /// </summary>
- private readonly object _initLock;
-
- private readonly ILogger<NetworkManager> _logger;
-
- private readonly IConfigurationManager _configurationManager;
-
- private readonly IConfiguration _startupConfig;
-
- private readonly object _networkEventLock;
-
- /// <summary>
- /// Holds the published server URLs and the IPs to use them on.
- /// </summary>
- private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
-
- private IReadOnlyList<IPNetwork> _remoteAddressFilter;
-
- /// <summary>
- /// Used to stop "event-racing conditions".
- /// </summary>
- private bool _eventfire;
-
- /// <summary>
- /// List of all interface MAC addresses.
- /// </summary>
- private IReadOnlyList<PhysicalAddress> _macAddresses;
-
- /// <summary>
- /// Dictionary containing interface addresses and their subnets.
- /// </summary>
- private IReadOnlyList<IPData> _interfaces;
-
- /// <summary>
- /// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>)
- /// or internal interface network subnets if undefined by user.
- /// </summary>
- private IReadOnlyList<IPNetwork> _lanSubnets;
-
- /// <summary>
- /// User defined list of subnets to excluded from the LAN.
- /// </summary>
- private IReadOnlyList<IPNetwork> _excludedSubnets;
-
- /// <summary>
- /// True if this object is disposed.
- /// </summary>
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="NetworkManager"/> class.
- /// </summary>
- /// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param>
- /// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param>
- /// <param name="logger">Logger to use for messages.</param>
-#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
- public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger)
- {
- ArgumentNullException.ThrowIfNull(logger);
- ArgumentNullException.ThrowIfNull(configurationManager);
-
- _logger = logger;
- _configurationManager = configurationManager;
- _startupConfig = startupConfig;
- _initLock = new();
- _interfaces = new List<IPData>();
- _macAddresses = new List<PhysicalAddress>();
- _publishedServerUrls = new List<PublishedServerUriOverride>();
- _networkEventLock = new object();
- _remoteAddressFilter = new List<IPNetwork>();
-
- UpdateSettings(_configurationManager.GetNetworkConfiguration());
-
- NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
- NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
-
- _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated;
- }
-#pragma warning restore CS8618 // Non-nullable field is uninitialized.
-
- /// <summary>
- /// Event triggered on network changes.
- /// </summary>
- public event EventHandler? NetworkChanged;
-
- /// <summary>
- /// Gets or sets a value indicating whether testing is taking place.
- /// </summary>
- public static string MockNetworkSettings { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets a value indicating whether IP4 is enabled.
- /// </summary>
- public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4;
-
- /// <summary>
- /// Gets a value indicating whether IP6 is enabled.
- /// </summary>
- public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6;
-
- /// <summary>
- /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
- /// </summary>
- public bool TrustAllIPv6Interfaces { get; private set; }
-
- /// <summary>
- /// Gets the Published server override list.
- /// </summary>
- public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
-
- /// <inheritdoc/>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Handler for network change events.
- /// </summary>
- /// <param name="sender">Sender.</param>
- /// <param name="e">A <see cref="NetworkAvailabilityEventArgs"/> containing network availability information.</param>
- private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e)
- {
- _logger.LogDebug("Network availability changed.");
- HandleNetworkChange();
- }
-
- /// <summary>
- /// Handler for network change events.
- /// </summary>
- /// <param name="sender">Sender.</param>
- /// <param name="e">An <see cref="EventArgs"/>.</param>
- private void OnNetworkAddressChanged(object? sender, EventArgs e)
- {
- _logger.LogDebug("Network address change detected.");
- HandleNetworkChange();
- }
-
- /// <summary>
- /// Triggers our event, and re-loads interface information.
- /// </summary>
- private void HandleNetworkChange()
- {
- lock (_networkEventLock)
- {
- if (!_eventfire)
- {
- // As network events tend to fire one after the other only fire once every second.
- _eventfire = true;
- OnNetworkChange();
- }
- }
- }
-
- /// <summary>
- /// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession.
- /// </summary>
- private void OnNetworkChange()
- {
- try
- {
- Thread.Sleep(2000);
- var networkConfig = _configurationManager.GetNetworkConfiguration();
- if (IsIPv6Enabled && !Socket.OSSupportsIPv6)
- {
- UpdateSettings(networkConfig);
- }
- else
- {
- InitializeInterfaces();
- InitializeLan(networkConfig);
- EnforceBindSettings(networkConfig);
- }
-
- PrintNetworkInformation(networkConfig);
- NetworkChanged?.Invoke(this, EventArgs.Empty);
- }
- finally
- {
- _eventfire = false;
- }
- }
-
- /// <summary>
- /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
- /// Generate a list of all active mac addresses that aren't loopback addresses.
- /// </summary>
- private void InitializeInterfaces()
- {
- lock (_initLock)
- {
- _logger.LogDebug("Refreshing interfaces.");
-
- var interfaces = new List<IPData>();
- var macAddresses = new List<PhysicalAddress>();
-
- try
- {
- var nics = NetworkInterface.GetAllNetworkInterfaces()
- .Where(i => i.OperationalStatus == OperationalStatus.Up);
-
- foreach (NetworkInterface adapter in nics)
- {
- try
- {
- var ipProperties = adapter.GetIPProperties();
- var mac = adapter.GetPhysicalAddress();
-
- // Populate MAC list
- if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac))
- {
- macAddresses.Add(mac);
- }
-
- // Populate interface list
- foreach (var info in ipProperties.UnicastAddresses)
- {
- if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
- {
- var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
- {
- Index = ipProperties.GetIPv4Properties().Index,
- Name = adapter.Name,
- SupportsMulticast = adapter.SupportsMulticast
- };
-
- interfaces.Add(interfaceObject);
- }
- else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
- {
- var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
- {
- Index = ipProperties.GetIPv6Properties().Index,
- Name = adapter.Name,
- SupportsMulticast = adapter.SupportsMulticast
- };
-
- interfaces.Add(interfaceObject);
- }
- }
- }
- catch (Exception ex)
- {
- // Ignore error, and attempt to continue.
- _logger.LogError(ex, "Error encountered parsing interfaces.");
- }
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error obtaining interfaces.");
- }
-
- // If no interfaces are found, fallback to loopback interfaces.
- if (interfaces.Count == 0)
- {
- _logger.LogWarning("No interface information available. Using loopback interface(s).");
-
- if (IsIPv4Enabled)
- {
- interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
- }
-
- if (IsIPv6Enabled)
- {
- interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
- }
- }
-
- _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count);
- _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString()));
-
- _macAddresses = macAddresses;
- _interfaces = interfaces;
- }
- }
-
- /// <summary>
- /// Initializes internal LAN cache.
- /// </summary>
- private void InitializeLan(NetworkConfiguration config)
- {
- lock (_initLock)
- {
- _logger.LogDebug("Refreshing LAN information.");
-
- // Get configuration options
- var subnets = config.LocalNetworkSubnets;
-
- // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN
- if (!NetworkExtensions.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0)
- {
- _logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
-
- var fallbackLanSubnets = new List<IPNetwork>();
- if (IsIPv6Enabled)
- {
- fallbackLanSubnets.Add(Network.IPv6RFC4291Loopback); // RFC 4291 (Loopback)
- fallbackLanSubnets.Add(Network.IPv6RFC4291SiteLocal); // RFC 4291 (Site local)
- fallbackLanSubnets.Add(Network.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local)
- }
-
- if (IsIPv4Enabled)
- {
- fallbackLanSubnets.Add(Network.IPv4RFC5735Loopback); // RFC 5735 (Loopback)
- fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A)
- fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B)
- fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C)
- }
-
- _lanSubnets = fallbackLanSubnets;
- }
- else
- {
- _lanSubnets = lanSubnets;
- }
-
- _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
- ? excludedSubnets
- : new List<IPNetwork>();
- }
- }
-
- /// <summary>
- /// Enforce bind addresses and exclusions on available interfaces.
- /// </summary>
- private void EnforceBindSettings(NetworkConfiguration config)
- {
- lock (_initLock)
- {
- // Respect explicit bind addresses
- var interfaces = _interfaces.ToList();
- var localNetworkAddresses = config.LocalNetworkAddresses;
- if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]))
- {
- var bindAddresses = localNetworkAddresses.Select(p => NetworkExtensions.TryParseToSubnet(p, out var network)
- ? network.Prefix
- : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase))
- .Select(x => x.Address)
- .FirstOrDefault() ?? IPAddress.None))
- .Where(x => x != IPAddress.None)
- .ToHashSet();
- interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
-
- if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
- {
- interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
- }
-
- if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
- {
- interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
- }
- }
-
- // Remove all interfaces matching any virtual machine interface prefix
- if (config.IgnoreVirtualInterfaces)
- {
- // Remove potentially existing * and split config string into prefixes
- var virtualInterfacePrefixes = config.VirtualInterfaceNames
- .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase));
-
- // Check all interfaces for matches against the prefixes and remove them
- if (_interfaces.Count > 0)
- {
- foreach (var virtualInterfacePrefix in virtualInterfacePrefixes)
- {
- interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase));
- }
- }
- }
-
- // Remove all IPv4 interfaces if IPv4 is disabled
- if (!IsIPv4Enabled)
- {
- interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork);
- }
-
- // Remove all IPv6 interfaces if IPv6 is disabled
- if (!IsIPv6Enabled)
- {
- interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
- }
-
- _interfaces = interfaces;
- }
- }
-
- /// <summary>
- /// Initializes the remote address values.
- /// </summary>
- private void InitializeRemote(NetworkConfiguration config)
- {
- lock (_initLock)
- {
- // Parse config values into filter collection
- var remoteIPFilter = config.RemoteIPFilter;
- if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First()))
- {
- // Parse all IPs with netmask to a subnet
- var remoteAddressFilter = new List<IPNetwork>();
- var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray();
- if (NetworkExtensions.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false))
- {
- remoteAddressFilter = remoteAddressFilterResult.ToList();
- }
-
- // Parse everything else as an IP and construct subnet with a single IP
- var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase));
- foreach (var ip in remoteFilteredIPs)
- {
- if (IPAddress.TryParse(ip, out var ipp))
- {
- remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize));
- }
- }
-
- _remoteAddressFilter = remoteAddressFilter;
- }
- }
- }
-
- /// <summary>
- /// Parses the user defined overrides into the dictionary object.
- /// Overrides are the equivalent of localised publishedServerUrl, enabling
- /// different addresses to be advertised over different subnets.
- /// format is subnet=ipaddress|host|uri
- /// when subnet = 0.0.0.0, any external address matches.
- /// </summary>
- private void InitializeOverrides(NetworkConfiguration config)
- {
- lock (_initLock)
- {
- var publishedServerUrls = new List<PublishedServerUriOverride>();
-
- // Prefer startup configuration.
- var startupOverrideKey = _startupConfig[AddressOverrideKey];
- if (!string.IsNullOrEmpty(startupOverrideKey))
- {
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- new IPData(IPAddress.Any, Network.IPv4Any),
- startupOverrideKey,
- true,
- true));
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- new IPData(IPAddress.IPv6Any, Network.IPv6Any),
- startupOverrideKey,
- true,
- true));
- _publishedServerUrls = publishedServerUrls;
- return;
- }
-
- var overrides = config.PublishedServerUriBySubnet;
- foreach (var entry in overrides)
- {
- var parts = entry.Split('=');
- if (parts.Length != 2)
- {
- _logger.LogError("Unable to parse bind override: {Entry}", entry);
- return;
- }
-
- var replacement = parts[1].Trim();
- var identifier = parts[0];
- if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
- {
- // Drop any other overrides in case an "all" override exists
- publishedServerUrls.Clear();
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- new IPData(IPAddress.Any, Network.IPv4Any),
- replacement,
- true,
- true));
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- new IPData(IPAddress.IPv6Any, Network.IPv6Any),
- replacement,
- true,
- true));
- break;
- }
- else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
- {
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- new IPData(IPAddress.Any, Network.IPv4Any),
- replacement,
- false,
- true));
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- new IPData(IPAddress.IPv6Any, Network.IPv6Any),
- replacement,
- false,
- true));
- }
- else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
- {
- foreach (var lan in _lanSubnets)
- {
- var lanPrefix = lan.Prefix;
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
- replacement,
- true,
- false));
- }
- }
- else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
- {
- var data = new IPData(result.Prefix, result);
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- data,
- replacement,
- true,
- true));
- }
- else if (TryParseInterface(identifier, out var ifaces))
- {
- foreach (var iface in ifaces)
- {
- publishedServerUrls.Add(
- new PublishedServerUriOverride(
- iface,
- replacement,
- true,
- true));
- }
- }
- else
- {
- _logger.LogError("Unable to parse bind override: {Entry}", entry);
- }
- }
-
- _publishedServerUrls = publishedServerUrls;
- }
- }
-
- private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
- {
- if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
- {
- UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
- }
- }
-
- /// <summary>
- /// Reloads all settings and re-Initializes the instance.
- /// </summary>
- /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
- public void UpdateSettings(object configuration)
- {
- ArgumentNullException.ThrowIfNull(configuration);
-
- var config = (NetworkConfiguration)configuration;
- HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
-
- InitializeLan(config);
- InitializeRemote(config);
-
- if (string.IsNullOrEmpty(MockNetworkSettings))
- {
- InitializeInterfaces();
- }
- else // Used in testing only.
- {
- // Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway.
- var interfaceList = MockNetworkSettings.Split('|');
- var interfaces = new List<IPData>();
- foreach (var details in interfaceList)
- {
- var parts = details.Split(',');
- if (NetworkExtensions.TryParseToSubnet(parts[0], out var subnet))
- {
- var address = subnet.Prefix;
- var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
- if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
- {
- var data = new IPData(address, subnet, parts[2])
- {
- Index = index
- };
- interfaces.Add(data);
- }
- }
- else
- {
- _logger.LogWarning("Could not parse mock interface settings: {Part}", details);
- }
- }
-
- _interfaces = interfaces;
- }
-
- EnforceBindSettings(config);
- InitializeOverrides(config);
-
- PrintNetworkInformation(config, false);
- }
-
- /// <summary>
- /// Protected implementation of Dispose pattern.
- /// </summary>
- /// <param name="disposing"><c>True</c> to dispose the managed state.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposed)
- {
- if (disposing)
- {
- _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated;
- NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
- NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged;
- }
-
- _disposed = true;
- }
- }
-
- /// <inheritdoc/>
- public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result)
- {
- if (string.IsNullOrEmpty(intf)
- || _interfaces is null
- || _interfaces.Count == 0)
- {
- result = null;
- return false;
- }
-
- // Match all interfaces starting with names starting with token
- result = _interfaces
- .Where(i => i.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)
- && ((IsIPv4Enabled && i.Address.AddressFamily == AddressFamily.InterNetwork)
- || (IsIPv6Enabled && i.Address.AddressFamily == AddressFamily.InterNetworkV6)))
- .OrderBy(x => x.Index)
- .ToArray();
- return result.Count > 0;
- }
-
- /// <inheritdoc/>
- public bool HasRemoteAccess(IPAddress remoteIP)
- {
- var config = _configurationManager.GetNetworkConfiguration();
- if (config.EnableRemoteAccess)
- {
- // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
- // If left blank, all remote addresses will be allowed.
- if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
- {
- // remoteAddressFilter is a whitelist or blacklist.
- var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
- if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
- || (config.IsRemoteIPFilterBlacklist && matches == 0))
- {
- return true;
- }
-
- return false;
- }
- }
- else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
- {
- // Remote not enabled. So everyone should be LAN.
- return false;
- }
-
- return true;
- }
-
- /// <inheritdoc/>
- public IReadOnlyList<PhysicalAddress> GetMacAddresses()
- {
- // Populated in construction - so always has values.
- return _macAddresses;
- }
-
- /// <inheritdoc/>
- public IReadOnlyList<IPData> GetLoopbacks()
- {
- if (!IsIPv4Enabled && !IsIPv6Enabled)
- {
- return Array.Empty<IPData>();
- }
-
- var loopbackNetworks = new List<IPData>();
- if (IsIPv4Enabled)
- {
- loopbackNetworks.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
- }
-
- if (IsIPv6Enabled)
- {
- loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
- }
-
- return loopbackNetworks;
- }
-
- /// <inheritdoc/>
- public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
- {
- if (_interfaces.Count > 0 || individualInterfaces)
- {
- return _interfaces;
- }
-
- // No bind address and no exclusions, so listen on all interfaces.
- var result = new List<IPData>();
- if (IsIPv4Enabled && IsIPv6Enabled)
- {
- // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
- result.Add(new IPData(IPAddress.IPv6Any, Network.IPv6Any));
- }
- else if (IsIPv4Enabled)
- {
- result.Add(new IPData(IPAddress.Any, Network.IPv4Any));
- }
- else if (IsIPv6Enabled)
- {
- // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too.
- foreach (var iface in _interfaces)
- {
- if (iface.AddressFamily == AddressFamily.InterNetworkV6)
- {
- result.Add(iface);
- }
- }
- }
-
- return result;
- }
-
- /// <inheritdoc/>
- public string GetBindAddress(string source, out int? port)
- {
- if (!NetworkExtensions.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
- {
- addresses = Array.Empty<IPAddress>();
- }
-
- var result = GetBindAddress(addresses.FirstOrDefault(), out port);
- return result;
- }
-
- /// <inheritdoc/>
- public string GetBindAddress(HttpRequest source, out int? port)
- {
- var result = GetBindAddress(source.Host.Host, out port);
- port ??= source.Host.Port;
-
- return result;
- }
-
- /// <inheritdoc/>
- public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false)
- {
- port = null;
-
- string result;
-
- if (source is not null)
- {
- if (IsIPv4Enabled && !IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6)
- {
- _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
- }
-
- if (!IsIPv4Enabled && IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetwork)
- {
- _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
- }
-
- bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
- _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
-
- if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
- {
- return result;
- }
-
- // No preference given, so move on to bind addresses.
- if (MatchesBindInterface(source, isExternal, out result))
- {
- return result;
- }
-
- if (isExternal && MatchesExternalInterface(source, out result))
- {
- return result;
- }
- }
-
- // Get the first LAN interface address that's not excluded and not a loopback address.
- // Get all available interfaces, prefer local interfaces
- var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address))
- .OrderByDescending(x => IsInLocalNetwork(x.Address))
- .ThenBy(x => x.Index)
- .ToList();
-
- if (availableInterfaces.Count == 0)
- {
- // There isn't any others, so we'll use the loopback.
- result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1";
- _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
- return result;
- }
-
- // If no source address is given, use the preferred (first) interface
- if (source is null)
- {
- result = NetworkExtensions.FormatIPString(availableInterfaces.First().Address);
- _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result);
- return result;
- }
-
- // Does the request originate in one of the interface subnets?
- // (For systems with multiple internal network cards, and multiple subnets)
- foreach (var intf in availableInterfaces)
- {
- if (intf.Subnet.Contains(source))
- {
- result = NetworkExtensions.FormatIPString(intf.Address);
- _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
- return result;
- }
- }
-
- // Fallback to first available interface
- result = NetworkExtensions.FormatIPString(availableInterfaces[0].Address);
- _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result);
- return result;
- }
-
- /// <inheritdoc/>
- public IReadOnlyList<IPData> GetInternalBindAddresses()
- {
- // Select all local bind addresses
- return _interfaces.Where(x => IsInLocalNetwork(x.Address))
- .OrderBy(x => x.Index)
- .ToList();
- }
-
- /// <inheritdoc/>
- public bool IsInLocalNetwork(string address)
- {
- if (NetworkExtensions.TryParseToSubnet(address, out var subnet))
- {
- return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
- }
-
- if (NetworkExtensions.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
- {
- foreach (var ept in addresses)
- {
- if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
- {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /// <inheritdoc/>
- public bool IsInLocalNetwork(IPAddress address)
- {
- ArgumentNullException.ThrowIfNull(address);
-
- // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
- if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
- || address.Equals(IPAddress.Loopback)
- || address.Equals(IPAddress.IPv6Loopback))
- {
- return true;
- }
-
- // As private addresses can be redefined by Configuration.LocalNetworkAddresses
- return CheckIfLanAndNotExcluded(address);
- }
-
- private bool CheckIfLanAndNotExcluded(IPAddress address)
- {
- foreach (var lanSubnet in _lanSubnets)
- {
- if (lanSubnet.Contains(address))
- {
- foreach (var excludedSubnet in _excludedSubnets)
- {
- if (excludedSubnet.Contains(address))
- {
- return false;
- }
- }
-
- return true;
- }
- }
-
- return false;
- }
-
- /// <summary>
- /// Attempts to match the source against the published server URL overrides.
- /// </summary>
- /// <param name="source">IP source address to use.</param>
- /// <param name="isInExternalSubnet">True if the source is in an external subnet.</param>
- /// <param name="bindPreference">The published server URL that matches the source address.</param>
- /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
- private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference)
- {
- bindPreference = string.Empty;
- int? port = null;
-
- // Only consider subnets including the source IP, prefering specific overrides
- List<PublishedServerUriOverride> validPublishedServerUrls;
- if (!isInExternalSubnet)
- {
- // Only use matching internal subnets
- // Prefer more specific (bigger subnet prefix) overrides
- validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
- .OrderByDescending(x => x.Data.Subnet.PrefixLength)
- .ToList();
- }
- else
- {
- // Only use matching external subnets
- // Prefer more specific (bigger subnet prefix) overrides
- validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
- .OrderByDescending(x => x.Data.Subnet.PrefixLength)
- .ToList();
- }
-
- foreach (var data in validPublishedServerUrls)
- {
- // Get interface matching override subnet
- var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
-
- if (intf?.Address is not null)
- {
- // If matching interface is found, use override
- bindPreference = data.OverrideUri;
- break;
- }
- }
-
- if (string.IsNullOrEmpty(bindPreference))
- {
- _logger.LogDebug("{Source}: No matching bind address override found", source);
- return false;
- }
-
- // Handle override specifying port
- var parts = bindPreference.Split(':');
- if (parts.Length > 1)
- {
- if (int.TryParse(parts[1], out int p))
- {
- bindPreference = parts[0];
- port = p;
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
- return true;
- }
- }
-
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
- return true;
- }
-
- /// <summary>
- /// Attempts to match the source against the user defined bind interfaces.
- /// </summary>
- /// <param name="source">IP source address to use.</param>
- /// <param name="isInExternalSubnet">True if the source is in the external subnet.</param>
- /// <param name="result">The result, if a match is found.</param>
- /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
- private bool MatchesBindInterface(IPAddress source, bool isInExternalSubnet, out string result)
- {
- result = string.Empty;
-
- int count = _interfaces.Count;
- if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any)))
- {
- // Ignore IPAny addresses.
- count = 0;
- }
-
- if (count == 0)
- {
- return false;
- }
-
- IPAddress? bindAddress = null;
- if (isInExternalSubnet)
- {
- var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
- .OrderBy(x => x.Index)
- .ToList();
- if (externalInterfaces.Count > 0)
- {
- // Check to see if any of the external bind interfaces are in the same subnet as the source.
- // If none exists, this will select the first external interface if there is one.
- bindAddress = externalInterfaces
- .OrderByDescending(x => x.Subnet.Contains(source))
- .ThenBy(x => x.Index)
- .Select(x => x.Address)
- .First();
-
- result = NetworkExtensions.FormatIPString(bindAddress);
- _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result);
- return true;
- }
-
- _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source);
- }
- else
- {
- // Check to see if any of the internal bind interfaces are in the same subnet as the source.
- // If none exists, this will select the first internal interface if there is one.
- bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
- .OrderByDescending(x => x.Subnet.Contains(source))
- .ThenBy(x => x.Index)
- .Select(x => x.Address)
- .FirstOrDefault();
-
- if (bindAddress is not null)
- {
- result = NetworkExtensions.FormatIPString(bindAddress);
- _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result);
- return true;
- }
- }
-
- return false;
- }
-
- /// <summary>
- /// Attempts to match the source against external interfaces.
- /// </summary>
- /// <param name="source">IP source address to use.</param>
- /// <param name="result">The result, if a match is found.</param>
- /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
- private bool MatchesExternalInterface(IPAddress source, out string result)
- {
- // Get the first external interface address that isn't a loopback.
- var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray();
-
- // No external interface found
- if (extResult.Length == 0)
- {
- result = string.Empty;
- _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source);
- return false;
- }
-
- // Does the request originate in one of the interface subnets?
- // (For systems with multiple network cards and/or multiple subnets)
- foreach (var intf in extResult)
- {
- if (intf.Subnet.Contains(source))
- {
- result = NetworkExtensions.FormatIPString(intf.Address);
- _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
- return true;
- }
- }
-
- // Fallback to first external interface.
- result = NetworkExtensions.FormatIPString(extResult[0].Address);
- _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
- return true;
- }
-
- private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
- {
- var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
- if (_logger.IsEnabled(logLevel))
- {
- _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
- _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
- _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
- }
- }
- }
-}
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index ce1c54cbb..54272aeaf 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -68,7 +68,6 @@ namespace Jellyfin.Server.Implementations.Activity
Date = entity.DateCreated,
Severity = entity.LogSeverity
})
- .AsQueryable()
.ToListAsync()
.ConfigureAwait(false));
}
@@ -80,11 +79,10 @@ namespace Jellyfin.Server.Implementations.Activity
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var entries = dbContext.ActivityLogs
- .Where(entry => entry.DateCreated <= startDate);
-
- dbContext.RemoveRange(entries);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ await dbContext.ActivityLogs
+ .Where(entry => entry.DateCreated <= startDate)
+ .ExecuteDeleteAsync()
+ .ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
index 2ee5b4e88..3f3a0dec5 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
@@ -1,7 +1,6 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using Jellyfin.Data.Events;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Authentication;
using MediaBrowser.Model.Activity;
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
index 27726a57a..8a33383e3 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
@@ -89,14 +90,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string GetPlaybackNotificationType(string mediaType)
+ private static string GetPlaybackNotificationType(MediaType mediaType)
{
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Audio)
{
return NotificationType.AudioPlayback.ToString();
}
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
return NotificationType.VideoPlayback.ToString();
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
index 6b16477aa..4c2effc2e 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
@@ -97,14 +98,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string? GetPlaybackStoppedNotificationType(string mediaType)
+ private static string? GetPlaybackStoppedNotificationType(MediaType mediaType)
{
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Audio)
{
return NotificationType.AudioPlaybackStopped.ToString();
}
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
return NotificationType.VideoPlaybackStopped.ToString();
}
diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
index 9a473de52..9626817e9 100644
--- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
@@ -1,5 +1,4 @@
-using Jellyfin.Data.Events;
-using Jellyfin.Data.Events.System;
+using Jellyfin.Data.Events.System;
using Jellyfin.Data.Events.Users;
using Jellyfin.Server.Implementations.Events.Consumers.Library;
using Jellyfin.Server.Implementations.Events.Consumers.Security;
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 390ed58b3..0ed1578c7 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -1,13 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
- <!-- Code analysers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index 0d91707e3..ea99af004 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
/// </summary>
public DbSet<User> Users => Set<User>();
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata.
+ /// </summary>
+ public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>();
+
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
new file mode 100644
index 000000000..28baf1992
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
@@ -0,0 +1,681 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230626233818_AddTrickplayInfos")]
+ partial class AddTrickplayInfos
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
new file mode 100644
index 000000000..76b12de08
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
@@ -0,0 +1,40 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddTrickplayInfos : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "TrickplayInfos",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ Width = table.Column<int>(type: "INTEGER", nullable: false),
+ Height = table.Column<int>(type: "INTEGER", nullable: false),
+ TileWidth = table.Column<int>(type: "INTEGER", nullable: false),
+ TileHeight = table.Column<int>(type: "INTEGER", nullable: false),
+ ThumbnailCount = table.Column<int>(type: "INTEGER", nullable: false),
+ Interval = table.Column<int>(type: "INTEGER", nullable: false),
+ Bandwidth = table.Column<int>(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width });
+ });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "TrickplayInfos");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
new file mode 100644
index 000000000..2884d4256
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
@@ -0,0 +1,654 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230923170422_UserCastReceiver")]
+ partial class UserCastReceiver
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
new file mode 100644
index 000000000..f06410c15
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class UserCastReceiver : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "CastReceiverId",
+ table: "Users",
+ type: "TEXT",
+ maxLength: 32,
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CastReceiverId",
+ table: "Users");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index d23508096..f725ababe 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -1,4 +1,4 @@
-// <auto-generated />
+// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("DeviceOptions");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
@@ -457,6 +488,10 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasMaxLength(255)
.HasColumnType("TEXT");
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
new file mode 100644
index 000000000..dc1c17e5e
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the TrickplayInfo entity.
+ /// </summary>
+ public class TrickplayInfoConfiguration : IEntityTypeConfiguration<TrickplayInfo>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<TrickplayInfo> builder)
+ {
+ builder.HasKey(info => new { info.ItemId, info.Width });
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
index b2dfe60a1..07ac27e3c 100644
--- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
@@ -58,19 +58,10 @@ namespace Jellyfin.Server.Implementations.Security
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var key = await dbContext.ApiKeys
+ await dbContext.ApiKeys
.Where(apiKey => apiKey.AccessToken == accessToken)
- .FirstOrDefaultAsync()
+ .ExecuteDeleteAsync()
.ConfigureAwait(false);
-
- if (key is null)
- {
- return;
- }
-
- dbContext.Remove(key);
-
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 77f8f7071..6bda12c5b 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -60,7 +60,7 @@ namespace Jellyfin.Server.Implementations.Security
}
private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary(
- IReadOnlyDictionary<string, string>? auth,
+ Dictionary<string, string>? auth,
IHeaderDictionary headers,
IQueryCollection queryString)
{
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
new file mode 100644
index 000000000..b960feb7f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -0,0 +1,474 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Trickplay;
+
+/// <summary>
+/// ITrickplayManager implementation.
+/// </summary>
+public class TrickplayManager : ITrickplayManager
+{
+ private readonly ILogger<TrickplayManager> _logger;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+ private readonly IImageEncoder _imageEncoder;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IApplicationPaths _appPaths;
+
+ private static readonly SemaphoreSlim _resourcePool = new(1, 1);
+ private static readonly string[] _trickplayImgExtensions = { ".jpg" };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayManager"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="fileSystem">The file systen.</param>
+ /// <param name="encodingHelper">The encoding helper.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="config">The server configuration manager.</param>
+ /// <param name="imageEncoder">The image encoder.</param>
+ /// <param name="dbProvider">The database provider.</param>
+ /// <param name="appPaths">The application paths.</param>
+ public TrickplayManager(
+ ILogger<TrickplayManager> logger,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ EncodingHelper encodingHelper,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager config,
+ IImageEncoder imageEncoder,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IApplicationPaths appPaths)
+ {
+ _logger = logger;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _encodingHelper = encodingHelper;
+ _libraryManager = libraryManager;
+ _config = config;
+ _imageEncoder = imageEncoder;
+ _dbProvider = dbProvider;
+ _appPaths = appPaths;
+ }
+
+ /// <inheritdoc />
+ public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
+
+ var options = _config.Configuration.TrickplayOptions;
+ foreach (var width in options.WidthResolutions)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await RefreshTrickplayDataInternal(
+ video,
+ replace,
+ width,
+ options,
+ cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task RefreshTrickplayDataInternal(
+ Video video,
+ bool replace,
+ int width,
+ TrickplayOptions options,
+ CancellationToken cancellationToken)
+ {
+ if (!CanGenerateTrickplay(video, options.Interval))
+ {
+ return;
+ }
+
+ var imgTempDir = string.Empty;
+ var outputDir = GetTrickplayDirectory(video, width);
+
+ await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
+ {
+ _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
+ return;
+ }
+
+ // Extract images
+ // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
+ var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
+
+ if (mediaSource is null)
+ {
+ _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
+ return;
+ }
+
+ var mediaPath = mediaSource.Path;
+ var mediaStream = mediaSource.VideoStream;
+ var container = mediaSource.Container;
+
+ _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
+ imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
+ mediaPath,
+ container,
+ mediaSource,
+ mediaStream,
+ width,
+ TimeSpan.FromMilliseconds(options.Interval),
+ options.EnableHwAcceleration,
+ options.ProcessThreads,
+ options.Qscale,
+ options.ProcessPriority,
+ _encodingHelper,
+ cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
+ {
+ throw new InvalidOperationException("Null or invalid directory from media encoder.");
+ }
+
+ var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
+ .Select(i => i.FullName)
+ .OrderBy(i => i)
+ .ToList();
+
+ // Create tiles
+ var trickplayInfo = CreateTiles(images, width, options, outputDir);
+
+ // Save tiles info
+ try
+ {
+ if (trickplayInfo is not null)
+ {
+ trickplayInfo.ItemId = video.Id;
+ await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
+
+ _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+ }
+ else
+ {
+ throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while saving trickplay tiles info.");
+
+ // Make sure no files stay in metadata folders on failure
+ // if tiles info wasn't saved.
+ Directory.Delete(outputDir, true);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating trickplay images.");
+ }
+ finally
+ {
+ _resourcePool.Release();
+
+ if (!string.IsNullOrEmpty(imgTempDir))
+ {
+ Directory.Delete(imgTempDir, true);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir)
+ {
+ if (images.Count == 0)
+ {
+ throw new ArgumentException("Can't create trickplay from 0 images.");
+ }
+
+ var workDir = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(workDir);
+
+ var trickplayInfo = new TrickplayInfo
+ {
+ Width = width,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
+ ThumbnailCount = images.Count,
+ // Set during image generation
+ Height = 0,
+ Bandwidth = 0
+ };
+
+ /*
+ * Generate trickplay tiles from sets of thumbnails
+ */
+ var imageOptions = new ImageCollageOptions
+ {
+ Width = trickplayInfo.TileWidth,
+ Height = trickplayInfo.TileHeight
+ };
+
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
+
+ for (int i = 0; i < requiredTiles; i++)
+ {
+ // Set output/input paths
+ var tilePath = Path.Combine(workDir, $"{i}.jpg");
+
+ imageOptions.OutputPath = tilePath;
+ imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile)));
+
+ // Generate image and use returned height for tiles info
+ var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
+ if (trickplayInfo.Height == 0)
+ {
+ trickplayInfo.Height = height;
+ }
+
+ // Update bitrate
+ var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
+ trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
+ }
+
+ /*
+ * Move trickplay tiles to output directory
+ */
+ Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
+
+ // Replace existing tiles if they already exist
+ if (Directory.Exists(outputDir))
+ {
+ Directory.Delete(outputDir, true);
+ }
+
+ MoveDirectory(workDir, outputDir);
+
+ return trickplayInfo;
+ }
+
+ private bool CanGenerateTrickplay(Video video, int interval)
+ {
+ var videoType = video.VideoType;
+ if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
+ {
+ return false;
+ }
+
+ if (video.IsPlaceHolder)
+ {
+ return false;
+ }
+
+ if (video.IsShortcut)
+ {
+ return false;
+ }
+
+ if (!video.IsCompleteMedia)
+ {
+ return false;
+ }
+
+ if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
+ {
+ return false;
+ }
+
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction)
+ {
+ return false;
+ }
+
+ // Can't extract images if there are no video streams
+ return video.GetMediaStreams().Count > 0;
+ }
+
+ /// <inheritdoc />
+ public async Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId)
+ {
+ var trickplayResolutions = new Dictionary<int, TrickplayInfo>();
+
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var trickplayInfos = await dbContext.TrickplayInfos
+ .AsNoTracking()
+ .Where(i => i.ItemId.Equals(itemId))
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ foreach (var info in trickplayInfos)
+ {
+ trickplayResolutions[info.Width] = info;
+ }
+ }
+
+ return trickplayResolutions;
+ }
+
+ /// <inheritdoc />
+ public async Task SaveTrickplayInfo(TrickplayInfo info)
+ {
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false);
+ if (oldInfo is not null)
+ {
+ dbContext.TrickplayInfos.Remove(oldInfo);
+ }
+
+ dbContext.Add(info);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+
+ /// <inheritdoc />
+ public async Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item)
+ {
+ var trickplayManifest = new Dictionary<string, Dictionary<int, TrickplayInfo>>();
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var mediaSourceId = Guid.Parse(mediaSource.Id);
+ var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false);
+
+ if (trickplayResolutions.Count > 0)
+ {
+ trickplayManifest[mediaSource.Id] = trickplayResolutions;
+ }
+ }
+
+ return trickplayManifest;
+ }
+
+ /// <inheritdoc />
+ public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ {
+ return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+ }
+
+ /// <inheritdoc />
+ public async Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey)
+ {
+ var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false);
+ if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
+ {
+ var builder = new StringBuilder(128);
+
+ if (trickplayInfo.ThumbnailCount > 0)
+ {
+ const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
+ const string decimalFormat = "{0:0.###}";
+
+ var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
+ var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}";
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var thumbnailDuration = trickplayInfo.Interval / 1000d;
+ var infDuration = thumbnailDuration * thumbnailsPerTile;
+ var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile);
+
+ builder
+ .AppendLine("#EXTM3U")
+ .Append("#EXT-X-TARGETDURATION:")
+ .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture))
+ .AppendLine("#EXT-X-VERSION:7")
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+ .AppendLine("#EXT-X-IMAGES-ONLY");
+
+ for (int i = 0; i < tileCount; i++)
+ {
+ // All tiles prior to the last must contain full amount of thumbnails (no black).
+ if (i == tileCount - 1)
+ {
+ thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile);
+ infDuration = thumbnailDuration * thumbnailsPerTile;
+ }
+
+ // EXTINF
+ builder
+ .Append("#EXTINF:")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
+ .AppendLine(",");
+
+ // EXT-X-TILES
+ builder
+ .Append("#EXT-X-TILES:RESOLUTION=")
+ .Append(resolution)
+ .Append(",LAYOUT=")
+ .Append(layout)
+ .Append(",DURATION=")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration)
+ .AppendLine();
+
+ // URL
+ builder
+ .AppendFormat(
+ CultureInfo.InvariantCulture,
+ urlFormat,
+ width.ToString(CultureInfo.InvariantCulture),
+ i.ToString(CultureInfo.InvariantCulture),
+ itemId.ToString("N"),
+ apiKey)
+ .AppendLine();
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return builder.ToString();
+ }
+ }
+
+ return null;
+ }
+
+ private string GetTrickplayDirectory(BaseItem item, int? width = null)
+ {
+ var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ }
+
+ private void MoveDirectory(string source, string destination)
+ {
+ try
+ {
+ Directory.Move(source, destination);
+ }
+ catch (IOException)
+ {
+ // Cross device move requires a copy
+ Directory.CreateDirectory(destination);
+ foreach (string file in Directory.GetFiles(source))
+ {
+ File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
+ }
+
+ Directory.Delete(source, true);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index bfae81e4c..edc6aa173 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller;
using Microsoft.EntityFrameworkCore;
@@ -13,7 +14,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <summary>
/// Manages the storage and retrieval of display preferences through Entity Framework.
/// </summary>
- public class DisplayPreferencesManager : IDisplayPreferencesManager
+ public sealed class DisplayPreferencesManager : IDisplayPreferencesManager, IAsyncDisposable
{
private readonly JellyfinDbContext _dbContext;
@@ -97,5 +98,11 @@ namespace Jellyfin.Server.Implementations.Users
{
_dbContext.SaveChanges();
}
+
+ /// <inheritdoc />
+ public async ValueTask DisposeAsync()
+ {
+ await _dbContext.DisposeAsync().ConfigureAwait(false);
+ }
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 94ac4798c..990b9a5bd 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
@@ -43,6 +44,7 @@ namespace Jellyfin.Server.Implementations.Users
private readonly InvalidAuthProvider _invalidAuthProvider;
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users;
@@ -55,13 +57,15 @@ namespace Jellyfin.Server.Implementations.Users
/// <param name="appHost">The application host.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
+ /// <param name="serverConfigurationManager">The system config manager.</param>
public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager,
INetworkManager networkManager,
IApplicationHost appHost,
IImageProcessor imageProcessor,
- ILogger<UserManager> logger)
+ ILogger<UserManager> logger,
+ IServerConfigurationManager serverConfigurationManager)
{
_dbProvider = dbProvider;
_eventManager = eventManager;
@@ -69,6 +73,7 @@ namespace Jellyfin.Server.Implementations.Users
_appHost = appHost;
_imageProcessor = imageProcessor;
_logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
@@ -103,7 +108,7 @@ namespace Jellyfin.Server.Implementations.Users
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
- [GeneratedRegex("^[\\w\\ \\-'._@]+$")]
+ [GeneratedRegex(@"^[\w\ \-'._@]+$")]
private static partial Regex ValidUsernameRegex();
/// <inheritdoc/>
@@ -288,6 +293,7 @@ namespace Jellyfin.Server.Implementations.Users
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
{
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
+ var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
return new UserDto
{
Name = user.Username,
@@ -315,7 +321,11 @@ namespace Jellyfin.Server.Implementations.Users
OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
- LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
+ LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
+ CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
+ ? castReceiverApplications.FirstOrDefault()?.Id
+ : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringComparison.Ordinal))?.Id
+ ?? castReceiverApplications.FirstOrDefault()?.Id
},
Policy = new UserPolicy
{
@@ -349,6 +359,7 @@ namespace Jellyfin.Server.Implementations.Users
ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
+ EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
@@ -604,6 +615,13 @@ namespace Jellyfin.Server.Implementations.Users
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+ // Only set cast receiver id if it is passed in and it exists in the server config.
+ if (!string.IsNullOrEmpty(config.CastReceiverId)
+ && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal)))
+ {
+ user.CastReceiverId = config.CastReceiverId;
+ }
+
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
@@ -666,6 +684,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
+ user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
@@ -729,7 +748,7 @@ namespace Jellyfin.Server.Implementations.Users
return GetPasswordResetProviders(user)[0];
}
- private IList<IAuthenticationProvider> GetAuthenticationProviders(User? user)
+ private List<IAuthenticationProvider> GetAuthenticationProviders(User? user)
{
var authenticationProviderId = user?.AuthenticationProviderId;
@@ -756,7 +775,7 @@ namespace Jellyfin.Server.Implementations.Users
return providers;
}
- private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
+ private IPasswordResetProvider[] GetPasswordResetProviders(User user)
{
var passwordResetProviderId = user.PasswordResetProviderId;
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 4c116745b..c12c90a68 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Security;
+using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
@@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Activity;
using MediaBrowser.Providers.Lyric;
using Microsoft.Extensions.Configuration;
@@ -78,6 +80,7 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+ serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
// TODO search the assemblies instead of adding them manually?
serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index b6af9baec..6066893de 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Jellyfin.Api.Middleware;
-using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.OpenApi.Models;
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index cb1680558..46df173bf 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -20,11 +20,10 @@ using Jellyfin.Api.Formatters;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
-using Jellyfin.Networking.Configuration;
-using Jellyfin.Networking.Constants;
-using Jellyfin.Networking.Extensions;
using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
+using MediaBrowser.Common.Api;
+using MediaBrowser.Common.Net;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authentication;
@@ -38,6 +37,7 @@ using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
+using IPNetwork = System.Net.IPNetwork;
namespace Jellyfin.Server.Extensions
{
@@ -82,6 +82,7 @@ namespace Jellyfin.Server.Extensions
options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
+ options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement));
options.AddPolicy(
Policies.RequiresElevation,
policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
@@ -245,6 +246,7 @@ namespace Jellyfin.Server.Extensions
// TODO - remove when all types are supported in System.Text.Json
c.AddSwaggerTypeMappings();
+ c.SchemaFilter<IgnoreEnumSchemaFilter>();
c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<FileResponseFilter>();
c.OperationFilter<FileRequestFilter>();
@@ -273,20 +275,20 @@ namespace Jellyfin.Server.Extensions
{
if (IPAddress.TryParse(allowedProxies[i], out var addr))
{
- AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize);
+ AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize);
}
- else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet))
+ else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet))
{
if (subnet is not null)
{
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
}
}
- else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
+ else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
{
foreach (var address in addresses)
{
- AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize);
+ AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize);
}
}
}
@@ -304,13 +306,13 @@ namespace Jellyfin.Server.Extensions
return;
}
- if (prefixLength == Network.MinimumIPv4PrefixSize)
+ if (prefixLength == NetworkConstants.MinimumIPv4PrefixSize)
{
options.KnownProxies.Add(addr);
}
else
{
- options.KnownNetworks.Add(new IPNetwork(addr, prefixLength));
+ options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(addr, prefixLength));
}
}
diff --git a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs
new file mode 100644
index 000000000..eb9ad03c2
--- /dev/null
+++ b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Jellyfin.Data.Attributes;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Jellyfin.Server.Filters;
+
+/// <summary>
+/// Filter to remove ignored enum values.
+/// </summary>
+public class IgnoreEnumSchemaFilter : ISchemaFilter
+{
+ /// <inheritdoc />
+ public void Apply(OpenApiSchema schema, SchemaFilterContext context)
+ {
+ if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false))
+ {
+ var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type);
+ if (type is null)
+ {
+ return;
+ }
+
+ var enumOpenApiStrings = new List<IOpenApiAny>();
+
+ foreach (var enumName in Enum.GetNames(type))
+ {
+ var member = type.GetMember(enumName)[0];
+ if (!member.GetCustomAttributes<OpenApiIgnoreEnumAttribute>().Any())
+ {
+ enumOpenApiStrings.Add(new OpenApiString(enumName));
+ }
+ }
+
+ schema.Enum = enumOpenApiStrings;
+ }
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 62abb8935..1d4d97551 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>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<ServerGarbageCollection>false</ServerGarbageCollection>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -22,8 +22,12 @@
<EmbeddedResource Include="Resources/Configuration/*" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 2db0b77cd..757b56a49 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -42,7 +42,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDownloadImagesInAdvance),
typeof(Routines.MigrateAuthenticationDb),
typeof(Routines.FixPlaylistOwner),
- typeof(Routines.MigrateRatingLevels)
+ typeof(Routines.MigrateRatingLevels),
+ typeof(Routines.AddDefaultCastReceivers)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
index c6d86b8cd..d92c00991 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
@@ -3,7 +3,7 @@ using System.IO;
using System.Xml;
using System.Xml.Serialization;
using Emby.Server.Implementations;
-using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
new file mode 100644
index 000000000..75a6a6176
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
@@ -0,0 +1,55 @@
+using System;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to add the default cast receivers to the system config.
+/// </summary>
+public class AddDefaultCastReceivers : IMigrationRoutine
+{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AddDefaultCastReceivers"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public AddDefaultCastReceivers(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc />
+ public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8");
+
+ /// <inheritdoc />
+ public string Name => "AddDefaultCastReceivers";
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => true;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ // Only add if receiver list is empty.
+ if (_serverConfigurationManager.Configuration.CastReceiverApplications.Length == 0)
+ {
+ _serverConfigurationManager.Configuration.CastReceiverApplications = new CastReceiverApplication[]
+ {
+ new()
+ {
+ Id = "F007D354",
+ Name = "Stable"
+ },
+ new()
+ {
+ Id = "6F511C87",
+ Name = "Unstable"
+ }
+ };
+
+ _serverConfigurationManager.SaveConfiguration();
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index e1a43bb48..247e1d845 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -3,7 +3,6 @@ using System.Globalization;
using System.IO;
using Emby.Server.Implementations.Data;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Globalization;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
@@ -79,11 +78,7 @@ namespace Jellyfin.Server.Migrations.Routines
}
else
{
- var ratingValue = _localizationManager.GetRatingLevel(ratingString).ToString();
- if (string.IsNullOrEmpty(ratingValue))
- {
- ratingValue = "NULL";
- }
+ var ratingValue = _localizationManager.GetRatingLevel(ratingString)?.ToString(CultureInfo.InvariantCulture) ?? "NULL";
using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
statement.TryBind("@Value", ratingValue);
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index f9259d0d9..c70ef1719 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -40,7 +40,7 @@ namespace Jellyfin.Server
/// </summary>
public const string LoggingConfigFileSystem = "logging.json";
- private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
+ private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory();
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 2acddb243..aa7be9109 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -4,10 +4,8 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
-using Emby.Dlna.Extensions;
using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions;
-using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.HappyEyeballs;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.HealthChecks;
@@ -36,7 +34,7 @@ namespace Jellyfin.Server
/// </summary>
public class Startup
{
- private readonly IServerApplicationHost _serverApplicationHost;
+ private readonly CoreAppHost _serverApplicationHost;
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
@@ -123,7 +121,6 @@ namespace Jellyfin.Server
.AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
services.AddHlsPlaylistGenerator();
- services.AddDlnaServices(_serverApplicationHost);
}
/// <summary>
diff --git a/Jellyfin.sln b/Jellyfin.sln
index cad23fc5e..4385ac241 100644
--- a/Jellyfin.sln
+++ b/Jellyfin.sln
@@ -23,10 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby.Dlna.csproj", "{805844AB-E92F-45E6-9D99-4F6D48D129A5}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}"
@@ -63,9 +59,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "src\Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
EndProject
@@ -139,14 +133,6 @@ Global
{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.Build.0 = Release|Any CPU
- {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.Build.0 = Release|Any CPU
- {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.Build.0 = Release|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -199,10 +185,6 @@ Global
{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU
- {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.Build.0 = Release|Any CPU
{30922383-D513-4F4D-B890-A940B57FA353}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30922383-D513-4F4D-B890-A940B57FA353}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30922383-D513-4F4D-B890-A940B57FA353}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -262,7 +244,6 @@ Global
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
- {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{30922383-D513-4F4D-B890-A940B57FA353} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
@@ -277,6 +258,7 @@ Global
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
{154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
+ {0A3FCC4D-C714-4072-B90F-E374A15F9FF9} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/MediaBrowser.Common/Api/Policies.cs
index 53841b0c4..e5427b8ef 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/MediaBrowser.Common/Api/Policies.cs
@@ -1,4 +1,4 @@
-namespace Jellyfin.Api.Constants;
+namespace MediaBrowser.Common.Api;
/// <summary>
/// Policies for the API authorization.
@@ -84,4 +84,9 @@ public static class Policies
/// Policy name for managing LiveTV.
/// </summary>
public const string LiveTvManagement = "LiveTvManagement";
+
+ /// <summary>
+ /// Policy name for accessing subtitles management.
+ /// </summary>
+ public const string SubtitleManagement = "SubtitleManagement";
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 7015d991f..8ad626b41 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -21,7 +21,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@@ -29,7 +28,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -43,8 +42,12 @@
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
- <!-- Code analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs
index c51090e38..78a391d36 100644
--- a/MediaBrowser.Common/Net/INetworkManager.cs
+++ b/MediaBrowser.Common/Net/INetworkManager.cs
@@ -82,7 +82,7 @@ namespace MediaBrowser.Common.Net
/// <param name="port">Optional port returned, if it's part of an override.</param>
/// <param name="skipOverrides">Optional boolean denoting if published server overrides should be ignored. Defaults to false.</param>
/// <returns>IP address to use, or loopback address if all else fails.</returns>
- string GetBindAddress(IPAddress source, out int? port, bool skipOverrides = false);
+ string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false);
/// <summary>
/// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo)
diff --git a/MediaBrowser.Common/Net/NetworkConfiguration.cs b/MediaBrowser.Common/Net/NetworkConfiguration.cs
new file mode 100644
index 000000000..61a51c99e
--- /dev/null
+++ b/MediaBrowser.Common/Net/NetworkConfiguration.cs
@@ -0,0 +1,175 @@
+#pragma warning disable CA1819 // Properties should not return arrays
+
+using System;
+
+namespace MediaBrowser.Common.Net;
+
+/// <summary>
+/// Defines the <see cref="NetworkConfiguration" />.
+/// </summary>
+public class NetworkConfiguration
+{
+ /// <summary>
+ /// The default value for <see cref="InternalHttpPort"/>.
+ /// </summary>
+ public const int DefaultHttpPort = 8096;
+
+ /// <summary>
+ /// The default value for <see cref="PublicHttpsPort"/> and <see cref="InternalHttpsPort"/>.
+ /// </summary>
+ public const int DefaultHttpsPort = 8920;
+
+ private string _baseUrl = string.Empty;
+
+ /// <summary>
+ /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
+ /// </summary>
+ public string BaseUrl
+ {
+ get => _baseUrl;
+ set
+ {
+ // Normalize the start of the string
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ // If baseUrl is empty, set an empty prefix string
+ _baseUrl = string.Empty;
+ return;
+ }
+
+ if (value[0] != '/')
+ {
+ // If baseUrl was not configured with a leading slash, append one for consistency
+ value = "/" + value;
+ }
+
+ // Normalize the end of the string
+ if (value[^1] == '/')
+ {
+ // If baseUrl was configured with a trailing slash, remove it for consistency
+ value = value.Remove(value.Length - 1);
+ }
+
+ _baseUrl = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to use HTTPS.
+ /// </summary>
+ /// <remarks>
+ /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
+ /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
+ /// </remarks>
+ public bool EnableHttps { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the server should force connections over HTTPS.
+ /// </summary>
+ public bool RequireHttps { get; set; }
+
+ /// <summary>
+ /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
+ /// </summary>
+ public string CertificatePath { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
+ /// </summary>
+ public string CertificatePassword { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the internal HTTP server port.
+ /// </summary>
+ /// <value>The HTTP server port.</value>
+ public int InternalHttpPort { get; set; } = DefaultHttpPort;
+
+ /// <summary>
+ /// Gets or sets the internal HTTPS server port.
+ /// </summary>
+ /// <value>The HTTPS server port.</value>
+ public int InternalHttpsPort { get; set; } = DefaultHttpsPort;
+
+ /// <summary>
+ /// Gets or sets the public HTTP port.
+ /// </summary>
+ /// <value>The public HTTP port.</value>
+ public int PublicHttpPort { get; set; } = DefaultHttpPort;
+
+ /// <summary>
+ /// Gets or sets the public HTTPS port.
+ /// </summary>
+ /// <value>The public HTTPS port.</value>
+ public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether Autodiscovery is enabled.
+ /// </summary>
+ public bool AutoDiscovery { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable automatic port forwarding.
+ /// </summary>
+ public bool EnableUPnP { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether IPv6 is enabled.
+ /// </summary>
+ public bool EnableIPv4 { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether IPv6 is enabled.
+ /// </summary>
+ public bool EnableIPv6 { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether access from outside of the LAN is permitted.
+ /// </summary>
+ public bool EnableRemoteAccess { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the subnets that are deemed to make up the LAN.
+ /// </summary>
+ public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.
+ /// </summary>
+ public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the known proxies.
+ /// </summary>
+ public string[] KnownProxies { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be ignored for the purposes of binding.
+ /// </summary>
+ public bool IgnoreVirtualInterfaces { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. <seealso cref="IgnoreVirtualInterfaces"/>.
+ /// </summary>
+ public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" };
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests.
+ /// </summary>
+ public bool EnablePublishedServerUriByRequest { get; set; } = false;
+
+ /// <summary>
+ /// Gets or sets the PublishedServerUriBySubnet
+ /// Gets or sets PublishedServerUri to advertise for specific subnets.
+ /// </summary>
+ public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>.
+ /// </summary>
+ public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist.
+ /// </summary>
+ public bool IsRemoteIPFilterBlacklist { get; set; }
+}
diff --git a/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs b/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs
new file mode 100644
index 000000000..9288964d2
--- /dev/null
+++ b/MediaBrowser.Common/Net/NetworkConfigurationExtensions.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Common.Configuration;
+
+namespace MediaBrowser.Common.Net;
+
+/// <summary>
+/// Defines the <see cref="NetworkConfigurationExtensions" />.
+/// </summary>
+public static class NetworkConfigurationExtensions
+{
+ /// <summary>
+ /// Retrieves the network configuration.
+ /// </summary>
+ /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+ /// <returns>The <see cref="NetworkConfiguration"/>.</returns>
+ public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config)
+ {
+ return config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
+ }
+}
diff --git a/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs b/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs
new file mode 100644
index 000000000..9309834f4
--- /dev/null
+++ b/MediaBrowser.Common/Net/NetworkConfigurationFactory.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+
+namespace MediaBrowser.Common.Net;
+
+/// <summary>
+/// Defines the <see cref="NetworkConfigurationFactory" />.
+/// </summary>
+public class NetworkConfigurationFactory : IConfigurationFactory
+{
+ /// <summary>
+ /// The GetConfigurations.
+ /// </summary>
+ /// <returns>The <see cref="IEnumerable{ConfigurationStore}"/>.</returns>
+ public IEnumerable<ConfigurationStore> GetConfigurations()
+ {
+ return new[]
+ {
+ new NetworkConfigurationStore()
+ };
+ }
+}
diff --git a/MediaBrowser.Common/Net/NetworkConfigurationStore.cs b/MediaBrowser.Common/Net/NetworkConfigurationStore.cs
new file mode 100644
index 000000000..d2f518707
--- /dev/null
+++ b/MediaBrowser.Common/Net/NetworkConfigurationStore.cs
@@ -0,0 +1,23 @@
+using MediaBrowser.Common.Configuration;
+
+namespace MediaBrowser.Common.Net;
+
+/// <summary>
+/// A configuration that stores network related settings.
+/// </summary>
+public class NetworkConfigurationStore : ConfigurationStore
+{
+ /// <summary>
+ /// The name of the configuration in the storage.
+ /// </summary>
+ public const string StoreKey = "network";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NetworkConfigurationStore"/> class.
+ /// </summary>
+ public NetworkConfigurationStore()
+ {
+ ConfigurationType = typeof(NetworkConfiguration);
+ Key = StoreKey;
+ }
+}
diff --git a/Jellyfin.Networking/Constants/Network.cs b/MediaBrowser.Common/Net/NetworkConstants.cs
index 7fadc74bb..b18058fa9 100644
--- a/Jellyfin.Networking/Constants/Network.cs
+++ b/MediaBrowser.Common/Net/NetworkConstants.cs
@@ -1,12 +1,12 @@
using System.Net;
-using Microsoft.AspNetCore.HttpOverrides;
+using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
-namespace Jellyfin.Networking.Constants;
+namespace MediaBrowser.Common.Net;
/// <summary>
/// Networking constants.
/// </summary>
-public static class Network
+public static class NetworkConstants
{
/// <summary>
/// IPv4 mask bytes.
diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkUtils.cs
index 2d921a482..e482089f0 100644
--- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs
+++ b/MediaBrowser.Common/Net/NetworkUtils.cs
@@ -1,20 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using Jellyfin.Extensions;
-using Jellyfin.Networking.Constants;
-using Microsoft.AspNetCore.HttpOverrides;
+using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
-namespace Jellyfin.Networking.Extensions;
+namespace MediaBrowser.Common.Net;
/// <summary>
-/// Defines the <see cref="NetworkExtensions" />.
+/// Defines the <see cref="NetworkUtils" />.
/// </summary>
-public static partial class NetworkExtensions
+public static partial class NetworkUtils
{
// Use regular expression as CheckHostName isn't RFC5892 compliant.
// Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
@@ -60,7 +58,7 @@ public static partial class NetworkExtensions
/// <returns>String value of the subnet mask in dotted decimal notation.</returns>
public static IPAddress CidrToMask(byte cidr, AddressFamily family)
{
- uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr);
+ uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize) - cidr);
addr = ((addr & 0xff000000) >> 24)
| ((addr & 0x00ff0000) >> 8)
| ((addr & 0x0000ff00) << 8)
@@ -76,7 +74,7 @@ public static partial class NetworkExtensions
/// <returns>String value of the subnet mask in dotted decimal notation.</returns>
public static IPAddress CidrToMask(int cidr, AddressFamily family)
{
- uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr);
+ uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize) - cidr);
addr = ((addr & 0xff000000) >> 24)
| ((addr & 0x00ff0000) >> 8)
| ((addr & 0x0000ff00) << 8)
@@ -101,7 +99,7 @@ public static partial class NetworkExtensions
}
// GetAddressBytes
- Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes];
+ Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.IPv4MaskBytes : NetworkConstants.IPv6MaskBytes];
if (!mask.TryWriteBytes(bytes, out var bytesWritten))
{
Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written.");
@@ -199,46 +197,29 @@ public static partial class NetworkExtensions
/// <returns><c>True</c> if parsing was successful.</returns>
public static bool TryParseToSubnet(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false)
{
- var splitString = value.Trim().Split('/');
- if (splitString.MoveNext())
+ value = value.Trim();
+ if (value.Contains('/'))
{
- var ipBlock = splitString.Current;
- var address = IPAddress.None;
- if (negated && ipBlock.StartsWith<char>("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
+ if (negated && value.StartsWith("!") && IPNetwork.TryParse(value[1..], out result))
{
- address = tmpAddress;
+ return true;
}
- else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress))
+ else if (!negated && IPNetwork.TryParse(value, out result))
{
- address = tmpAddress;
+ return true;
}
-
- if (address != IPAddress.None)
+ }
+ else if (IPAddress.TryParse(value, out var address))
+ {
+ if (address.AddressFamily == AddressFamily.InterNetwork)
{
- if (splitString.MoveNext())
- {
- var subnetBlock = splitString.Current;
- if (int.TryParse(subnetBlock, out var netmask))
- {
- result = new IPNetwork(address, netmask);
- return true;
- }
- else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress))
- {
- result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress));
- return true;
- }
- }
- else if (address.AddressFamily == AddressFamily.InterNetwork)
- {
- result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
- return true;
- }
- else if (address.AddressFamily == AddressFamily.InterNetworkV6)
- {
- result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
- return true;
- }
+ result = address.Equals(IPAddress.Any) ? NetworkConstants.IPv4Any : new IPNetwork(address, NetworkConstants.MinimumIPv4PrefixSize);
+ return true;
+ }
+ else if (address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ result = address.Equals(IPAddress.IPv6Any) ? NetworkConstants.IPv6Any : new IPNetwork(address, NetworkConstants.MinimumIPv6PrefixSize);
+ return true;
}
}
diff --git a/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs b/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs
deleted file mode 100644
index 3afe874c5..000000000
--- a/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace MediaBrowser.Common.Plugins
-{
- using Microsoft.Extensions.DependencyInjection;
-
- /// <summary>
- /// Defines the <see cref="IPluginServiceRegistrator" />.
- /// </summary>
- public interface IPluginServiceRegistrator
- {
- /// <summary>
- /// Registers the plugin's services with the service collection.
- /// </summary>
- /// <remarks>
- /// This interface is only used for service registration and requires a parameterless constructor.
- /// </remarks>
- /// <param name="serviceCollection">The service collection.</param>
- void RegisterServices(IServiceCollection serviceCollection);
- }
-}
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index b263c173e..6acab13fe 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -1,5 +1,4 @@
using System;
-using System.Linq;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
index ac20120d9..975218ad7 100644
--- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
@@ -1,4 +1,3 @@
-using System.Threading;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
deleted file mode 100644
index 06da5ea09..000000000
--- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Model.Dlna;
-using Microsoft.AspNetCore.Http;
-
-namespace MediaBrowser.Controller.Dlna
-{
- public interface IDlnaManager
- {
- /// <summary>
- /// Gets the profile infos.
- /// </summary>
- /// <returns>IEnumerable{DeviceProfileInfo}.</returns>
- IEnumerable<DeviceProfileInfo> GetProfileInfos();
-
- /// <summary>
- /// Gets the profile.
- /// </summary>
- /// <param name="headers">The headers.</param>
- /// <returns>DeviceProfile.</returns>
- DeviceProfile? GetProfile(IHeaderDictionary headers);
-
- /// <summary>
- /// Gets the default profile.
- /// </summary>
- /// <returns>DeviceProfile.</returns>
- DeviceProfile GetDefaultProfile();
-
- /// <summary>
- /// Creates the profile.
- /// </summary>
- /// <param name="profile">The profile.</param>
- void CreateProfile(DeviceProfile profile);
-
- /// <summary>
- /// Updates the profile.
- /// </summary>
- /// <param name="profileId">The profile id.</param>
- /// <param name="profile">The profile.</param>
- void UpdateProfile(string profileId, DeviceProfile profile);
-
- /// <summary>
- /// Deletes the profile.
- /// </summary>
- /// <param name="id">The identifier.</param>
- void DeleteProfile(string id);
-
- /// <summary>
- /// Gets the profile.
- /// </summary>
- /// <param name="id">The identifier.</param>
- /// <returns>DeviceProfile.</returns>
- DeviceProfile? GetProfile(string id);
-
- /// <summary>
- /// Gets the profile.
- /// </summary>
- /// <param name="deviceInfo">The device information.</param>
- /// <returns>DeviceProfile.</returns>
- DeviceProfile? GetProfile(DeviceIdentification deviceInfo);
-
- /// <summary>
- /// Gets the server description XML.
- /// </summary>
- /// <param name="headers">The headers.</param>
- /// <param name="serverUuId">The server uu identifier.</param>
- /// <param name="serverAddress">The server address.</param>
- /// <returns>System.String.</returns>
- string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress);
-
- /// <summary>
- /// Gets the icon.
- /// </summary>
- /// <param name="filename">The filename.</param>
- /// <returns>DlnaIconResponse.</returns>
- ImageStream? GetIcon(string filename);
- }
-}
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index e5c8ebfaf..c7bfbdb53 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -81,5 +81,15 @@ namespace MediaBrowser.Controller.Drawing
/// <param name="posters">The list of poster paths.</param>
/// <param name="backdrops">The list of backdrop paths.</param>
void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops);
+
+ /// <summary>
+ /// Creates a new trickplay tile image.
+ /// </summary>
+ /// <param name="options">The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels.</param>
+ /// <param name="quality">The image encode quality.</param>
+ /// <param name="imgWidth">The width of a single trickplay thumbnail.</param>
+ /// <param name="imgHeight">Optional height of a single trickplay thumbnail, if it is known.</param>
+ /// <returns>Height of single decoded trickplay thumbnail.</returns>
+ int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
}
}
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index c7216a320..243d2f04f 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -63,7 +63,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// </summary>
/// <value>The type of the media.</value>
[JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Audio;
+ public override MediaType MediaType => MediaType.Audio;
public override double GetDefaultPrimaryImageAspectRatio()
{
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 9f3e8eec9..7c04fcbfc 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -422,7 +422,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <value>The type of the media.</value>
[JsonIgnore]
- public virtual string MediaType => null;
+ public virtual MediaType MediaType => MediaType.Unknown;
[JsonIgnore]
public virtual string[] PhysicalLocations
@@ -724,7 +724,7 @@ namespace MediaBrowser.Controller.Entities
if (this is IHasCollectionType view)
{
- if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase))
+ if (view.CollectionType == CollectionType.livetv)
{
return true;
}
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index 615d236c7..dcd22a3b4 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -95,10 +95,7 @@ namespace MediaBrowser.Controller.Entities
}
var p = destProps.Find(x => x.Name == sourceProp.Name);
- if (p is not null)
- {
- p.SetValue(dest, v);
- }
+ p?.SetValue(dest, v);
}
}
diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
index afafaf1c2..4bf21061c 100644
--- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs
+++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Controller.Entities
{
@@ -11,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
public abstract class BasePluginFolder : Folder, ICollectionFolder
{
[JsonIgnore]
- public virtual string? CollectionType => null;
+ public virtual CollectionType? CollectionType => null;
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs
index d75beb06d..66dea1084 100644
--- a/MediaBrowser.Controller/Entities/Book.cs
+++ b/MediaBrowser.Controller/Entities/Book.cs
@@ -18,13 +18,16 @@ namespace MediaBrowser.Controller.Entities
}
[JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Book;
+ public override MediaType MediaType => MediaType.Book;
public override bool SupportsPlayedStatus => true;
public override bool SupportsPositionTicksResume => true;
[JsonIgnore]
+ public override bool SupportsPeople => true;
+
+ [JsonIgnore]
public string SeriesPresentationUniqueKey { get; set; }
[JsonIgnore]
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index f51162f9d..992bb19bb 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
@@ -69,7 +70,7 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
- public string CollectionType { get; set; }
+ public CollectionType? CollectionType { get; set; }
/// <summary>
/// Gets the item's children.
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 44fe65103..e707eedbf 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -598,7 +598,7 @@ namespace MediaBrowser.Controller.Entities
for (var i = 0; i < childrenCount; i++)
{
- await actionBlock.SendAsync(i).ConfigureAwait(false);
+ await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false);
}
actionBlock.Complete();
diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
index 89e494ebc..742691b00 100644
--- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
@@ -3,6 +3,7 @@
#pragma warning disable CA1819, CS1591
using System;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Controller.Entities
{
@@ -27,6 +28,6 @@ namespace MediaBrowser.Controller.Entities
public interface IHasCollectionType
{
- string CollectionType { get; }
+ CollectionType? CollectionType { get; }
}
}
diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
index 14459624e..7a73f3eaf 100644
--- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
+++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
@@ -1,5 +1,7 @@
#nullable disable
+using Jellyfin.Data.Enums;
+
namespace MediaBrowser.Controller.Entities
{
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index a51299284..555dd050c 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -36,13 +36,13 @@ namespace MediaBrowser.Controller.Entities
ImageTypes = Array.Empty<ImageType>();
IncludeItemTypes = Array.Empty<BaseItemKind>();
ItemIds = Array.Empty<Guid>();
- MediaTypes = Array.Empty<string>();
+ MediaTypes = Array.Empty<MediaType>();
MinSimilarityScore = 20;
OfficialRatings = Array.Empty<string>();
- OrderBy = Array.Empty<(string, SortOrder)>();
+ OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
PersonIds = Array.Empty<Guid>();
PersonTypes = Array.Empty<string>();
- PresetViews = Array.Empty<string>();
+ PresetViews = Array.Empty<CollectionType?>();
SeriesStatuses = Array.Empty<SeriesStatus>();
SourceTypes = Array.Empty<SourceType>();
StudioIds = Array.Empty<Guid>();
@@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.Entities
public bool? IncludeItemsByName { get; set; }
- public string[] MediaTypes { get; set; }
+ public MediaType[] MediaTypes { get; set; }
public BaseItemKind[] IncludeItemTypes { get; set; }
@@ -248,7 +248,7 @@ namespace MediaBrowser.Controller.Entities
public Guid[] TopParentIds { get; set; }
- public string[] PresetViews { get; set; }
+ public CollectionType?[] PresetViews { get; set; }
public TrailerType[] TrailerTypes { get; set; }
@@ -284,7 +284,7 @@ namespace MediaBrowser.Controller.Entities
public bool? HasChapterImages { get; set; }
- public IReadOnlyList<(string OrderBy, SortOrder SortOrder)> OrderBy { get; set; }
+ public IReadOnlyList<(ItemSortBy OrderBy, SortOrder SortOrder)> OrderBy { get; set; }
public DateTime? MinDateCreated { get; set; }
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 66210cb6c..d7ccfd8ae 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.Entities.Movies
{
public BoxSet()
{
- DisplayOrder = ItemSortBy.PremiereDate;
+ DisplayOrder = "PremiereDate";
}
[JsonIgnore]
@@ -116,13 +116,13 @@ namespace MediaBrowser.Controller.Entities.Movies
{
var children = base.GetChildren(user, includeLinkedChildren, query);
- if (string.Equals(DisplayOrder, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(DisplayOrder, "SortName", StringComparison.OrdinalIgnoreCase))
{
// Sort by name
return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
}
- if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase))
{
// Sort by release date
return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
@@ -136,7 +136,7 @@ namespace MediaBrowser.Controller.Entities.Movies
{
var children = base.GetRecursiveChildren(user, query);
- if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase))
{
// Sort by release date
return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs
index ba6ce189a..cb9feacd3 100644
--- a/MediaBrowser.Controller/Entities/Photo.cs
+++ b/MediaBrowser.Controller/Entities/Photo.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
namespace MediaBrowser.Controller.Entities
@@ -13,7 +14,7 @@ namespace MediaBrowser.Controller.Entities
public override bool SupportsLocalMetadata => false;
[JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Photo;
+ public override MediaType MediaType => MediaType.Photo;
[JsonIgnore]
public override Folder LatestItemsIndexContainer => AlbumEntity;
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index 47432ee93..eb026deb4 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Querying;
@@ -16,21 +17,21 @@ namespace MediaBrowser.Controller.Entities
{
public class UserView : Folder, IHasCollectionType
{
- private static readonly string[] _viewTypesEligibleForGrouping = new string[]
+ private static readonly CollectionType?[] _viewTypesEligibleForGrouping =
{
- Model.Entities.CollectionType.Movies,
- Model.Entities.CollectionType.TvShows,
- string.Empty
+ Jellyfin.Data.Enums.CollectionType.movies,
+ Jellyfin.Data.Enums.CollectionType.tvshows,
+ null
};
- private static readonly string[] _originalFolderViewTypes = new string[]
+ private static readonly CollectionType?[] _originalFolderViewTypes =
{
- Model.Entities.CollectionType.Books,
- Model.Entities.CollectionType.MusicVideos,
- Model.Entities.CollectionType.HomeVideos,
- Model.Entities.CollectionType.Photos,
- Model.Entities.CollectionType.Music,
- Model.Entities.CollectionType.BoxSets
+ Jellyfin.Data.Enums.CollectionType.books,
+ Jellyfin.Data.Enums.CollectionType.musicvideos,
+ Jellyfin.Data.Enums.CollectionType.homevideos,
+ Jellyfin.Data.Enums.CollectionType.photos,
+ Jellyfin.Data.Enums.CollectionType.music,
+ Jellyfin.Data.Enums.CollectionType.boxsets
};
public static ITVSeriesManager TVSeriesManager { get; set; }
@@ -38,7 +39,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets or sets the view type.
/// </summary>
- public string ViewType { get; set; }
+ public CollectionType? ViewType { get; set; }
/// <summary>
/// Gets or sets the display parent id.
@@ -52,7 +53,7 @@ namespace MediaBrowser.Controller.Entities
/// <inheritdoc />
[JsonIgnore]
- public string CollectionType => ViewType;
+ public CollectionType? CollectionType => ViewType;
/// <inheritdoc />
[JsonIgnore]
@@ -160,7 +161,7 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- return string.Equals(Model.Entities.CollectionType.Playlists, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase);
+ return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.playlists;
}
public static bool IsEligibleForGrouping(Folder folder)
@@ -169,14 +170,14 @@ namespace MediaBrowser.Controller.Entities
&& IsEligibleForGrouping(collectionFolder.CollectionType);
}
- public static bool IsEligibleForGrouping(string viewType)
+ public static bool IsEligibleForGrouping(CollectionType? viewType)
{
- return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ return _viewTypesEligibleForGrouping.Contains(viewType);
}
- public static bool EnableOriginalFolder(string viewType)
+ public static bool EnableOriginalFolder(CollectionType? viewType)
{
- return _originalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ return _originalFolderViewTypes.Contains(viewType);
}
protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken)
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index c276ab463..a3525c862 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -42,7 +42,7 @@ namespace MediaBrowser.Controller.Entities
_tvSeriesManager = tvSeriesManager;
}
- public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query)
+ public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, CollectionType? viewType, InternalItemsQuery query)
{
var user = query.User;
@@ -58,58 +58,58 @@ namespace MediaBrowser.Controller.Entities
switch (viewType)
{
- case CollectionType.Folders:
+ case CollectionType.folders:
return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query);
- case CollectionType.TvShows:
+ case CollectionType.tvshows:
return GetTvView(queryParent, user, query);
- case CollectionType.Movies:
+ case CollectionType.movies:
return GetMovieFolders(queryParent, user, query);
- case SpecialFolder.TvShowSeries:
+ case CollectionType.tvshowseries:
return GetTvSeries(queryParent, user, query);
- case SpecialFolder.TvGenres:
+ case CollectionType.tvgenres:
return GetTvGenres(queryParent, user, query);
- case SpecialFolder.TvGenre:
+ case CollectionType.tvgenre:
return GetTvGenreItems(queryParent, displayParent, user, query);
- case SpecialFolder.TvResume:
+ case CollectionType.tvresume:
return GetTvResume(queryParent, user, query);
- case SpecialFolder.TvNextUp:
+ case CollectionType.tvnextup:
return GetTvNextUp(queryParent, query);
- case SpecialFolder.TvLatest:
+ case CollectionType.tvlatest:
return GetTvLatest(queryParent, user, query);
- case SpecialFolder.MovieFavorites:
+ case CollectionType.moviefavorites:
return GetFavoriteMovies(queryParent, user, query);
- case SpecialFolder.MovieLatest:
+ case CollectionType.movielatest:
return GetMovieLatest(queryParent, user, query);
- case SpecialFolder.MovieGenres:
+ case CollectionType.moviegenres:
return GetMovieGenres(queryParent, user, query);
- case SpecialFolder.MovieGenre:
+ case CollectionType.moviegenre:
return GetMovieGenreItems(queryParent, displayParent, user, query);
- case SpecialFolder.MovieResume:
+ case CollectionType.movieresume:
return GetMovieResume(queryParent, user, query);
- case SpecialFolder.MovieMovies:
+ case CollectionType.moviemovies:
return GetMovieMovies(queryParent, user, query);
- case SpecialFolder.MovieCollections:
+ case CollectionType.moviecollection:
return GetMovieCollections(user, query);
- case SpecialFolder.TvFavoriteEpisodes:
+ case CollectionType.tvfavoriteepisodes:
return GetFavoriteEpisodes(queryParent, user, query);
- case SpecialFolder.TvFavoriteSeries:
+ case CollectionType.tvfavoriteseries:
return GetFavoriteSeries(queryParent, user, query);
default:
@@ -146,12 +146,12 @@ namespace MediaBrowser.Controller.Entities
var list = new List<BaseItem>
{
- GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent),
- GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent),
- GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent),
- GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent),
- GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent),
- GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)
+ GetUserView(CollectionType.movieresume, "HeaderContinueWatching", "0", parent),
+ GetUserView(CollectionType.movielatest, "Latest", "1", parent),
+ GetUserView(CollectionType.moviemovies, "Movies", "2", parent),
+ GetUserView(CollectionType.moviecollection, "Collections", "3", parent),
+ GetUserView(CollectionType.moviefavorites, "Favorites", "4", parent),
+ GetUserView(CollectionType.moviegenres, "Genres", "5", parent)
};
return GetResult(list, query);
@@ -264,7 +264,7 @@ namespace MediaBrowser.Controller.Entities
}
})
.Where(i => i is not null)
- .Select(i => GetUserViewWithName(SpecialFolder.MovieGenre, i.SortName, parent));
+ .Select(i => GetUserViewWithName(CollectionType.moviegenre, i.SortName, parent));
return GetResult(genres, query);
}
@@ -303,13 +303,13 @@ namespace MediaBrowser.Controller.Entities
var list = new List<BaseItem>
{
- GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent),
- GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent),
- GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent),
- GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent),
- GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent),
- GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent),
- GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)
+ GetUserView(CollectionType.tvresume, "HeaderContinueWatching", "0", parent),
+ GetUserView(CollectionType.tvnextup, "HeaderNextUp", "1", parent),
+ GetUserView(CollectionType.tvlatest, "Latest", "2", parent),
+ GetUserView(CollectionType.tvshowseries, "Shows", "3", parent),
+ GetUserView(CollectionType.tvfavoriteseries, "HeaderFavoriteShows", "4", parent),
+ GetUserView(CollectionType.tvfavoriteepisodes, "HeaderFavoriteEpisodes", "5", parent),
+ GetUserView(CollectionType.tvgenres, "Genres", "6", parent)
};
return GetResult(list, query);
@@ -330,7 +330,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query)
{
- var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty });
+ var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.tvshows });
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@@ -392,7 +392,7 @@ namespace MediaBrowser.Controller.Entities
}
})
.Where(i => i is not null)
- .Select(i => GetUserViewWithName(SpecialFolder.TvGenre, i.SortName, parent));
+ .Select(i => GetUserViewWithName(CollectionType.tvgenre, i.SortName, parent));
return GetResult(genres, query);
}
@@ -476,7 +476,7 @@ namespace MediaBrowser.Controller.Entities
public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager)
{
- if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType))
{
return false;
}
@@ -943,7 +943,7 @@ namespace MediaBrowser.Controller.Entities
.Where(i => user.IsFolderGrouped(i.Id) && UserView.IsEligibleForGrouping(i));
}
- private BaseItem[] GetMediaFolders(User user, IEnumerable<string> viewTypes)
+ private BaseItem[] GetMediaFolders(User user, IEnumerable<CollectionType> viewTypes)
{
if (user is null)
{
@@ -952,7 +952,7 @@ namespace MediaBrowser.Controller.Entities
{
var folder = i as ICollectionFolder;
- return folder is not null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ return folder?.CollectionType is not null && viewTypes.Contains(folder.CollectionType.Value);
}).ToArray();
}
@@ -961,11 +961,11 @@ namespace MediaBrowser.Controller.Entities
{
var folder = i as ICollectionFolder;
- return folder is not null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ return folder?.CollectionType is not null && viewTypes.Contains(folder.CollectionType.Value);
}).ToArray();
}
- private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable<string> viewTypes)
+ private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable<CollectionType> viewTypes)
{
if (parent is null || parent is UserView)
{
@@ -975,12 +975,12 @@ namespace MediaBrowser.Controller.Entities
return new BaseItem[] { parent };
}
- private UserView GetUserViewWithName(string type, string sortName, BaseItem parent)
+ private UserView GetUserViewWithName(CollectionType? type, string sortName, BaseItem parent)
{
- return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N", CultureInfo.InvariantCulture), type, sortName);
+ return _userViewManager.GetUserSubView(parent.Id, type, parent.Id.ToString("N", CultureInfo.InvariantCulture), sortName);
}
- private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent)
+ private UserView GetUserView(CollectionType? type, string localizationKey, string sortName, BaseItem parent)
{
return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName);
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 9f685b7e2..be2eb4d28 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -9,6 +9,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@@ -256,7 +257,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <value>The type of the media.</value>
[JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Video;
+ public override MediaType MediaType => MediaType.Video;
public override List<string> GetUserDataKeys()
{
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index f34e3d68d..9ec22324f 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.Library
IDirectoryService directoryService,
Folder parent,
LibraryOptions libraryOptions,
- string collectionType = null);
+ CollectionType? collectionType = null);
/// <summary>
/// Gets a Person.
@@ -199,9 +199,9 @@ namespace MediaBrowser.Controller.Library
/// <param name="sortBy">The sort by.</param>
/// <param name="sortOrder">The sort order.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
- IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder);
+ IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder);
- IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy);
+ IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy);
/// <summary>
/// Gets the user root folder.
@@ -256,28 +256,28 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="item">The item.</param>
/// <returns>System.String.</returns>
- string GetContentType(BaseItem item);
+ CollectionType? GetContentType(BaseItem item);
/// <summary>
/// Gets the type of the inherited content.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>System.String.</returns>
- string GetInheritedContentType(BaseItem item);
+ CollectionType? GetInheritedContentType(BaseItem item);
/// <summary>
/// Gets the type of the configured content.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>System.String.</returns>
- string GetConfiguredContentType(BaseItem item);
+ CollectionType? GetConfiguredContentType(BaseItem item);
/// <summary>
/// Gets the type of the configured content.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.String.</returns>
- string GetConfiguredContentType(string path);
+ CollectionType? GetConfiguredContentType(string path);
/// <summary>
/// Normalizes the root path list.
@@ -329,7 +329,7 @@ namespace MediaBrowser.Controller.Library
User user,
string name,
Guid parentId,
- string viewType,
+ CollectionType? viewType,
string sortName);
/// <summary>
@@ -343,7 +343,7 @@ namespace MediaBrowser.Controller.Library
UserView GetNamedView(
User user,
string name,
- string viewType,
+ CollectionType? viewType,
string sortName);
/// <summary>
@@ -355,7 +355,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>The named view.</returns>
UserView GetNamedView(
string name,
- string viewType,
+ CollectionType viewType,
string sortName);
/// <summary>
@@ -370,7 +370,7 @@ namespace MediaBrowser.Controller.Library
UserView GetNamedView(
string name,
Guid parentId,
- string viewType,
+ CollectionType? viewType,
string sortName,
string uniqueId);
@@ -383,7 +383,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>The shadow view.</returns>
UserView GetShadowView(
BaseItem parent,
- string viewType,
+ CollectionType? viewType,
string sortName);
/// <summary>
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index 034c40591..43cccfc65 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -35,6 +35,15 @@ namespace MediaBrowser.Controller.Library
void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
+ /// <summary>
+ /// Save the provided user data for the given user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="userDataDto">The reason for updating the user data.</param>
+ /// <param name="reason">The reason.</param>
+ void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason);
+
UserItemData GetUserData(User user, BaseItem item);
UserItemData GetUserData(Guid userId, BaseItem item);
diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs
index 055627d3e..a565dc88b 100644
--- a/MediaBrowser.Controller/Library/IUserViewManager.cs
+++ b/MediaBrowser.Controller/Library/IUserViewManager.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Library;
@@ -28,7 +29,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="localizationKey">Localization key to use.</param>
/// <param name="sortName">Sort to use.</param>
/// <returns>User view.</returns>
- UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName);
+ UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName);
/// <summary>
/// Gets latest items.
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index dcd0110fb..6202f92f5 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
@@ -120,7 +121,7 @@ namespace MediaBrowser.Controller.Library
}
}
- public string CollectionType { get; set; }
+ public CollectionType? CollectionType { get; set; }
public bool HasParent<T>()
where T : Folder
@@ -220,7 +221,7 @@ namespace MediaBrowser.Controller.Library
return GetFileSystemEntryByName(name) is not null;
}
- public string GetCollectionType()
+ public CollectionType? GetCollectionType()
{
return CollectionType;
}
@@ -229,7 +230,7 @@ namespace MediaBrowser.Controller.Library
/// Gets the configured content type for the path.
/// </summary>
/// <returns>The configured content type.</returns>
- public string GetConfiguredContentType()
+ public CollectionType? GetConfiguredContentType()
{
return _libraryManager.GetConfiguredContentType(Path);
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index f11e3c8f6..3c2cf8e3d 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.LiveTv
public override LocationType LocationType => LocationType.Remote;
[JsonIgnore]
- public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
+ public override MediaType MediaType => ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video;
[JsonIgnore]
public bool IsMovie { get; set; }
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index c721fb778..05540d490 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.LiveTv
{
public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes
{
- private static string EmbyServiceName = "Emby";
+ private const string EmbyServiceName = "Emby";
public LiveTvProgram()
{
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 69c0d26b6..f237993fd 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -20,7 +20,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
</ItemGroup>
@@ -35,7 +34,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -49,8 +48,12 @@
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index fba347bda..6a16d421c 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -20,6 +20,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Configuration;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
namespace MediaBrowser.Controller.MediaEncoding
{
@@ -100,6 +101,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "truehd", 6 },
};
+ private static readonly string _defaultMjpegEncoder = "mjpeg";
+ private static readonly Dictionary<string, string> _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "vaapi", _defaultMjpegEncoder + "_vaapi" },
+ { "qsv", _defaultMjpegEncoder + "_qsv" }
+ };
+
public static readonly string[] LosslessAudioCodecs = new string[]
{
"alac",
@@ -167,6 +175,24 @@ namespace MediaBrowser.Controller.MediaEncoding
return defaultEncoder;
}
+ private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ if (state.VideoType == VideoType.VideoFile)
+ {
+ var hwType = encodingOptions.HardwareAccelerationType;
+
+ if (!string.IsNullOrEmpty(hwType)
+ && encodingOptions.EnableHardwareEncoding
+ && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder)
+ && _mediaEncoder.SupportsEncoder(preferredEncoder))
+ {
+ return preferredEncoder;
+ }
+ }
+
+ return _defaultMjpegEncoder;
+ }
+
private bool IsVaapiSupported(EncodingJobInfo state)
{
// vaapi will throw an error with this input
@@ -300,6 +326,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetH264Encoder(state, encodingOptions);
}
+ if (string.Equals(codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetMjpegEncoder(state, encodingOptions);
+ }
+
if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
{
@@ -745,12 +776,17 @@ namespace MediaBrowser.Controller.MediaEncoding
private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias)
{
alias ??= VaapiAlias;
- renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
- var driverOpts = string.IsNullOrEmpty(driver)
- ? ":" + renderNodePath
- : ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
+
+ // 'renderNodePath' has higher priority than 'kernelDriver'
+ var driverOpts = string.IsNullOrEmpty(renderNodePath)
+ ? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver)
+ : renderNodePath;
+
+ // 'driver' behaves similarly to env LIBVA_DRIVER_NAME
+ driverOpts += string.IsNullOrEmpty(driver) ? string.Empty : ",driver=" + driver;
+
var options = string.IsNullOrEmpty(srcDeviceAlias)
- ? driverOpts
+ ? (string.IsNullOrEmpty(driverOpts) ? string.Empty : ":" + driverOpts)
: "@" + srcDeviceAlias;
return string.Format(
@@ -872,14 +908,14 @@ namespace MediaBrowser.Controller.MediaEncoding
if (_mediaEncoder.IsVaapiDeviceInteliHD)
{
- args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, VaapiAlias));
}
else if (_mediaEncoder.IsVaapiDeviceInteli965)
{
// Only override i965 since it has lower priority than iHD in libva lookup.
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
- args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, VaapiAlias));
}
var filterDevArgs = string.Empty;
@@ -1745,11 +1781,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Values 0-3, 0 being highest quality but slower
var profileScore = 0;
- string crf;
var qmin = "0";
var qmax = "50";
-
- crf = "10";
+ var crf = "10";
if (isVc1)
{
@@ -2947,7 +2981,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
+ @"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2",
maxWidthParam,
maxHeightParam,
scaleVal);
@@ -2989,7 +3023,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
+ @"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2",
maxWidthParam,
scaleVal);
}
@@ -3001,7 +3035,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
+ @"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})",
maxHeightParam,
scaleVal);
}
@@ -3021,19 +3055,19 @@ namespace MediaBrowser.Controller.MediaEncoding
switch (threedFormat.Value)
{
case Video3DFormat.HalfSideBySide:
- filter = "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={0}:trunc({0}/dar/2)*2";
+ filter = @"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={0}:trunc({0}/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 requestedWidth. 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:
- filter = "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={0}:trunc({0}/dar/2)*2";
+ filter = @"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={0}:trunc({0}/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 requestedWidth.
break;
case Video3DFormat.HalfTopAndBottom:
- filter = "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={0}:trunc({0}/dar/2)*2";
+ filter = @"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={0}:trunc({0}/dar/2)*2";
// htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth
break;
case Video3DFormat.FullTopAndBottom:
- filter = "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={0}:trunc({0}/dar/2)*2";
+ filter = @"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={0}:trunc({0}/dar/2)*2";
// ftab crop height in half, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth
break;
default:
@@ -3309,7 +3343,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// [0:s]scale=s=1280x720
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
- overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
return (mainFilters, subFilters, overlayFilters);
@@ -3486,7 +3520,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
subFilters.Add("hwupload=derive_device=cuda");
- overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay_cuda=eof_action=pass:repeatlast=0");
}
}
else
@@ -3495,7 +3529,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
- overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@@ -3684,7 +3718,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
subFilters.Add("hwupload=derive_device=opencl");
- overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0");
overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1");
overlayFilters.Add("format=d3d11");
}
@@ -3695,7 +3729,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
- overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@@ -3930,7 +3964,7 @@ namespace MediaBrowser.Controller.MediaEncoding
: string.Empty;
var overlayQsvFilter = string.Format(
CultureInfo.InvariantCulture,
- "overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}",
+ "overlay_qsv=eof_action=pass:repeatlast=0{0}",
overlaySize);
overlayFilters.Add(overlayQsvFilter);
}
@@ -3941,7 +3975,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
- overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@@ -4146,7 +4180,7 @@ namespace MediaBrowser.Controller.MediaEncoding
: string.Empty;
var overlayQsvFilter = string.Format(
CultureInfo.InvariantCulture,
- "overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}",
+ "overlay_qsv=eof_action=pass:repeatlast=0{0}",
overlaySize);
overlayFilters.Add(overlayQsvFilter);
}
@@ -4157,7 +4191,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
- overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@@ -4411,7 +4445,7 @@ namespace MediaBrowser.Controller.MediaEncoding
: string.Empty;
var overlayVaapiFilter = string.Format(
CultureInfo.InvariantCulture,
- "overlay_vaapi=eof_action=endall:shortest=1:repeatlast=0{0}",
+ "overlay_vaapi=eof_action=pass:repeatlast=0{0}",
overlaySize);
overlayFilters.Add(overlayVaapiFilter);
}
@@ -4422,7 +4456,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
- overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
if (isVaapiEncoder)
{
@@ -4582,7 +4616,7 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add("hwupload=derive_device=vulkan");
subFilters.Add("format=vulkan");
- overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay_vulkan=eof_action=pass:repeatlast=0");
if (isSwEncoder)
{
@@ -4783,7 +4817,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
- overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
+ overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
if (isVaapiEncoder)
{
@@ -4919,6 +4953,15 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
overlayFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
+ var framerate = GetFramerateParam(state);
+ if (framerate.HasValue)
+ {
+ mainFilters.Insert(0, string.Format(
+ CultureInfo.InvariantCulture,
+ "fps={0}",
+ framerate.Value));
+ }
+
var mainStr = string.Empty;
if (mainFilters?.Count > 0)
{
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 4114dea4f..c2cef4978 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -138,6 +140,36 @@ namespace MediaBrowser.Controller.MediaEncoding
Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken);
/// <summary>
+ /// Extracts the video images on interval.
+ /// </summary>
+ /// <param name="inputFile">Input file.</param>
+ /// <param name="container">Video container type.</param>
+ /// <param name="mediaSource">Media source information.</param>
+ /// <param name="imageStream">Media stream information.</param>
+ /// <param name="maxWidth">The maximum width.</param>
+ /// <param name="interval">The interval.</param>
+ /// <param name="allowHwAccel">Allow for hardware acceleration.</param>
+ /// <param name="threads">The input/output thread count for ffmpeg.</param>
+ /// <param name="qualityScale">The qscale value for ffmpeg.</param>
+ /// <param name="priority">The process priority for the ffmpeg process.</param>
+ /// <param name="encodingHelper">EncodingHelper instance.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Directory where images where extracted. A given image made before another will always be named with a lower number.</returns>
+ Task<string> ExtractVideoImagesOnIntervalAccelerated(
+ string inputFile,
+ string container,
+ MediaSourceInfo mediaSource,
+ MediaStream imageStream,
+ int maxWidth,
+ TimeSpan interval,
+ bool allowHwAccel,
+ int? threads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
+ EncodingHelper encodingHelper,
+ CancellationToken cancellationToken);
+
+ /// <summary>
/// Gets the media info.
/// </summary>
/// <param name="request">The request.</param>
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index e0942e490..0a706c307 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// The logger.
/// </summary>
- protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
+ protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
{
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 498df5ab0..ca032e7f6 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -69,7 +69,7 @@ namespace MediaBrowser.Controller.Playlists
public override bool SupportsInheritedParentImages => false;
[JsonIgnore]
- public override bool SupportsPlayedStatus => string.Equals(MediaType, "Video", StringComparison.OrdinalIgnoreCase);
+ public override bool SupportsPlayedStatus => MediaType == Jellyfin.Data.Enums.MediaType.Video;
[JsonIgnore]
public override bool AlwaysScanInternalMetadataPath => true;
@@ -80,10 +80,10 @@ namespace MediaBrowser.Controller.Playlists
[JsonIgnore]
public override bool IsPreSorted => true;
- public string PlaylistMediaType { get; set; }
+ public MediaType PlaylistMediaType { get; set; }
[JsonIgnore]
- public override string MediaType => PlaylistMediaType;
+ public override MediaType MediaType => PlaylistMediaType;
[JsonIgnore]
private bool IsSharedItem
@@ -107,9 +107,9 @@ namespace MediaBrowser.Controller.Playlists
return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
}
- public void SetMediaType(string value)
+ public void SetMediaType(MediaType? value)
{
- PlaylistMediaType = value;
+ PlaylistMediaType = value ?? MediaType.Unknown;
}
public override double GetDefaultPrimaryImageAspectRatio()
@@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Playlists
return base.GetChildren(user, true, query);
}
- public static List<BaseItem> GetPlaylistItems(string playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
+ public static List<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
{
if (user is not null)
{
@@ -185,7 +185,7 @@ namespace MediaBrowser.Controller.Playlists
return list;
}
- private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, string mediaType, DtoOptions options)
+ private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options)
{
if (item is MusicGenre musicGenre)
{
diff --git a/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs b/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs
new file mode 100644
index 000000000..8b62f3808
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/IPluginServiceRegistrator.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace MediaBrowser.Controller.Plugins;
+
+/// <summary>
+/// Defines the <see cref="IPluginServiceRegistrator" />.
+/// </summary>
+/// <remarks>
+/// This interface is only used for service registration and requires a parameterless constructor.
+/// </remarks>
+public interface IPluginServiceRegistrator
+{
+ /// <summary>
+ /// Registers the plugin's services with the service collection.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <param name="applicationHost">The server application host.</param>
+ void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost);
+}
diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
index c4ad352a3..2f7ebb5cc 100644
--- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs
+++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Controller.Providers
{
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 16943f6aa..eb5069b06 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index 282aa721e..0699734c4 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -32,7 +33,7 @@ namespace MediaBrowser.Controller.Resolvers
MultiItemResolverResult ResolveMultiple(
Folder parent,
List<FileSystemMetadata> files,
- string collectionType,
+ CollectionType? collectionType,
IDirectoryService directoryService);
}
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 25bf23d61..3e30c8dc4 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
@@ -60,13 +61,13 @@ namespace MediaBrowser.Controller.Session
/// Gets the playable media types.
/// </summary>
/// <value>The playable media types.</value>
- public IReadOnlyList<string> PlayableMediaTypes
+ public IReadOnlyList<MediaType> PlayableMediaTypes
{
get
{
if (Capabilities is null)
{
- return Array.Empty<string>();
+ return Array.Empty<MediaType>();
}
return Capabilities.PlayableMediaTypes;
@@ -110,6 +111,12 @@ namespace MediaBrowser.Controller.Session
public DateTime LastPlaybackCheckIn { get; set; }
/// <summary>
+ /// Gets or sets the last paused date.
+ /// </summary>
+ /// <value>The last paused date.</value>
+ public DateTime? LastPausedDate { get; set; }
+
+ /// <summary>
/// Gets or sets the name of the device.
/// </summary>
/// <value>The name of the device.</value>
diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
index 07fe1ea8a..96f8a2af5 100644
--- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Sorting
@@ -9,9 +10,8 @@ namespace MediaBrowser.Controller.Sorting
public interface IBaseItemComparer : IComparer<BaseItem?>
{
/// <summary>
- /// Gets the name.
+ /// Gets the comparer type.
/// </summary>
- /// <value>The name.</value>
- string Name { get; }
+ ItemSortBy Type { get; }
}
}
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
new file mode 100644
index 000000000..0c41f3023
--- /dev/null
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Trickplay;
+
+/// <summary>
+/// Interface ITrickplayManager.
+/// </summary>
+public interface ITrickplayManager
+{
+ /// <summary>
+ /// Generates new trickplay images and metadata.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="replace">Whether or not existing data should be replaced.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Task.</returns>
+ Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Creates trickplay tiles out of individual thumbnails.
+ /// </summary>
+ /// <param name="images">Ordered file paths of the thumbnails to be used.</param>
+ /// <param name="width">The width of a single thumbnail.</param>
+ /// <param name="options">The trickplay options.</param>
+ /// <param name="outputDir">The output directory.</param>
+ /// <returns>The associated trickplay information.</returns>
+ /// <remarks>
+ /// The output directory will be DELETED and replaced if it already exists.
+ /// </remarks>
+ TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir);
+
+ /// <summary>
+ /// Get available trickplay resolutions and corresponding info.
+ /// </summary>
+ /// <param name="itemId">The item.</param>
+ /// <returns>Map of width resolutions to trickplay tiles info.</returns>
+ Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId);
+
+ /// <summary>
+ /// Saves trickplay info.
+ /// </summary>
+ /// <param name="info">The trickplay info.</param>
+ /// <returns>Task.</returns>
+ Task SaveTrickplayInfo(TrickplayInfo info);
+
+ /// <summary>
+ /// Gets all trickplay infos for all media streams of an item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>A map of media source id to a map of tile width to trickplay info.</returns>
+ Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item);
+
+ /// <summary>
+ /// Gets the path to a trickplay tile image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="width">The width of a single thumbnail.</param>
+ /// <param name="index">The tile's index.</param>
+ /// <returns>The absolute path.</returns>
+ string GetTrickplayTilePath(BaseItem item, int width, int index);
+
+ /// <summary>
+ /// Gets the trickplay HLS playlist.
+ /// </summary>
+ /// <param name="itemId">The item.</param>
+ /// <param name="width">The width of a single thumbnail.</param>
+ /// <param name="apiKey">Optional api key of the requesting user.</param>
+ /// <returns>The text content of the .m3u8 playlist.</returns>
+ Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey);
+}
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 71cdea529..05177ac39 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -11,7 +11,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -20,8 +20,12 @@
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
index 879a3616b..e0277870d 100644
--- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
@@ -1,5 +1,7 @@
+using System;
using System.Collections.Generic;
using System.Xml;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Playlists;
@@ -31,7 +33,11 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "PlaylistMediaType":
- item.PlaylistMediaType = reader.ReadNormalizedString();
+ if (Enum.TryParse<MediaType>(reader.ReadNormalizedString(), out var mediaType))
+ {
+ item.PlaylistMediaType = mediaType;
+ }
+
break;
case "PlaylistItems":
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index f913b2320..5a7193079 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -6,6 +6,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
index 847add07f..3f018cae9 100644
--- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
@@ -1,6 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using System.Xml;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -48,12 +49,12 @@ namespace MediaBrowser.LocalMetadata.Savers
{
var game = (Playlist)item;
- if (string.IsNullOrEmpty(game.PlaylistMediaType))
+ if (game.PlaylistMediaType == MediaType.Unknown)
{
return Task.CompletedTask;
}
- return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType);
+ return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString());
}
/// <inheritdoc />
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index 9e7a1d50a..1f94d9b23 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo;
/// </summary>
public class BdInfoFileInfo : BDInfo.IO.IFileInfo
{
- private FileSystemMetadata _impl;
+ private readonly FileSystemMetadata _impl;
/// <summary>
/// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class.
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index db119ce5c..0d1d27ae8 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -121,7 +121,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"yadif_videotoolbox"
};
- private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
+ private static readonly Dictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
{
{ 0, new string[] { "scale_cuda", "Output format (default \"same\")" } },
{ 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
@@ -132,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
};
// These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
- private static readonly IReadOnlyDictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
+ private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
{
{ "libavutil", new Version(56, 14) },
{ "libavcodec", new Version(58, 18) },
@@ -197,7 +197,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
internal bool ValidateVersionInternal(string versionOutput)
{
- if (versionOutput.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
+ if (versionOutput.Contains("Libav developers", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
return false;
@@ -333,7 +333,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="output">The 'ffmpeg -version' output.</param>
/// <returns>The library names and major.minor version numbers.</returns>
- private static IReadOnlyDictionary<string, Version> GetFFmpegLibraryVersions(string output)
+ private static Dictionary<string, Version> GetFFmpegLibraryVersions(string output)
{
var map = new Dictionary<string, Version>();
@@ -499,8 +499,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders;
- var found = Regex
- .Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline)
+ var found = CodecRegex()
+ .Matches(output)
.Select(x => x.Groups["codec"].Value)
.Where(x => required.Contains(x));
@@ -527,8 +527,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
return Enumerable.Empty<string>();
}
- var found = Regex
- .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline)
+ var found = FilterRegex()
+ .Matches(output)
.Select(x => x.Groups["filter"].Value)
.Where(x => _requiredFilters.Contains(x));
@@ -537,9 +537,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
return found;
}
- private IDictionary<int, bool> GetFFmpegFiltersWithOption()
+ private Dictionary<int, bool> GetFFmpegFiltersWithOption()
{
- IDictionary<int, bool> dict = new Dictionary<int, bool>();
+ Dictionary<int, bool> dict = new Dictionary<int, bool>();
for (int i = 0; i < _filterOptionsDict.Count; i++)
{
if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2)
@@ -582,5 +582,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
return reader.ReadToEnd();
}
}
+
+ [GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
+ private static partial Regex CodecRegex();
+
+ [GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
+ private static partial Regex FilterRegex();
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
index 04128c911..c5f500e76 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs
@@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <returns>System.String.</returns>
private static string GetFileInputArgument(string path, string inputPrefix)
{
- if (path.IndexOf("://", StringComparison.Ordinal) != -1)
+ if (path.Contains("://", StringComparison.Ordinal))
{
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path);
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 99c49e4ae..4dbefca4b 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -21,6 +21,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Probing;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -28,8 +29,10 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
+using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static Nikse.SubtitleEdit.Core.Common.IfoParser;
namespace MediaBrowser.MediaEncoding.Encoder
{
@@ -177,7 +180,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (_ffmpegPath is not null)
{
// Determine a probe path from the mpeg path
- _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, @"ffprobe$1");
+ _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1");
// Interrogate to understand what coders are supported
var validator = new EncoderValidator(_logger, _ffmpegPath);
@@ -415,14 +418,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
{
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
- var analyzeDuration = string.Empty;
+ var extraArgs = GetExtraArguments(request);
+
+ return GetMediaInfoInternal(
+ GetInputArgument(request.MediaSource.Path, request.MediaSource),
+ request.MediaSource.Path,
+ request.MediaSource.Protocol,
+ extractChapters,
+ extraArgs,
+ request.MediaType == DlnaProfileType.Audio,
+ request.MediaSource.VideoType,
+ cancellationToken);
+ }
+
+ internal string GetExtraArguments(MediaInfoRequest request)
+ {
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
+ var analyzeDuration = string.Empty;
var extraArgs = string.Empty;
if (request.MediaSource.AnalyzeDurationMs > 0)
{
- analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
+ analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
}
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
{
@@ -439,15 +457,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
extraArgs += " -probesize " + ffmpegProbeSize;
}
- return GetMediaInfoInternal(
- GetInputArgument(request.MediaSource.Path, request.MediaSource),
- request.MediaSource.Path,
- request.MediaSource.Protocol,
- extractChapters,
- extraArgs,
- request.MediaType == DlnaProfileType.Audio,
- request.MediaSource.VideoType,
- cancellationToken);
+ if (request.MediaSource.RequiredHttpHeaders.TryGetValue("user_agent", out var userAgent))
+ {
+ extraArgs += " -user_agent " + userAgent;
+ }
+
+ return extraArgs;
}
/// <inheritdoc />
@@ -623,9 +638,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private string GetImageResolutionParameter()
{
- string imageResolutionParameter;
-
- imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
+ var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
{
ImageResolution.P144 => "256x144",
ImageResolution.P240 => "426x240",
@@ -670,13 +683,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
var scaler = threedFormat switch
{
// 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.
- Video3DFormat.HalfSideBySide => "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",
+ Video3DFormat.HalfSideBySide => @"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",
// fsbs crop width in half,set the display aspect,crop out any black bars we may have made
- Video3DFormat.FullSideBySide => "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",
+ Video3DFormat.FullSideBySide => @"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",
// htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made
- Video3DFormat.HalfTopAndBottom => "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",
+ Video3DFormat.HalfTopAndBottom => @"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",
// ftab crop height in half, set the display aspect,crop out any black bars we may have made
- Video3DFormat.FullTopAndBottom => "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",
+ Video3DFormat.FullTopAndBottom => @"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=trunc(iw*sar):ih"
};
@@ -783,6 +796,191 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
/// <inheritdoc />
+ public Task<string> ExtractVideoImagesOnIntervalAccelerated(
+ string inputFile,
+ string container,
+ MediaSourceInfo mediaSource,
+ MediaStream imageStream,
+ int maxWidth,
+ TimeSpan interval,
+ bool allowHwAccel,
+ int? threads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
+ EncodingHelper encodingHelper,
+ CancellationToken cancellationToken)
+ {
+ var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
+ threads ??= _threads;
+
+ // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
+ // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
+ if (!allowHwAccel)
+ {
+ options.EnableHardwareEncoding = false;
+ options.HardwareAccelerationType = string.Empty;
+ options.EnableTonemapping = false;
+ }
+
+ var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
+ var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
+ {
+ IsVideoRequest = true, // must be true for InputVideoHwaccelArgs to return non-empty value
+ MediaSource = mediaSource,
+ VideoStream = imageStream,
+ BaseRequest = baseRequest, // GetVideoProcessingFilterParam errors if null
+ MediaPath = inputFile,
+ OutputVideoCodec = "mjpeg"
+ };
+ var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec;
+
+ // Get input and filter arguments
+ var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
+ if (string.IsNullOrWhiteSpace(inputArg))
+ {
+ throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
+ }
+
+ if (!allowHwAccel)
+ {
+ inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
+ }
+
+ var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim();
+ if (string.IsNullOrWhiteSpace(filterParam))
+ {
+ throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
+ }
+
+ return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken);
+ }
+
+ private async Task<string> ExtractVideoImagesOnIntervalInternal(
+ string inputArg,
+ string filterParam,
+ string vidEncoder,
+ int? outputThreads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(inputArg))
+ {
+ throw new InvalidOperationException("Empty or invalid input argument.");
+ }
+
+ // Output arguments
+ var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(targetDirectory);
+ var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
+
+ // Final command arguments
+ var args = string.Format(
+ CultureInfo.InvariantCulture,
+ "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}-f {5} \"{6}\"",
+ inputArg,
+ filterParam,
+ outputThreads.GetValueOrDefault(_threads),
+ vidEncoder,
+ qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
+ "image2",
+ outputPath);
+
+ // Start ffmpeg process
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = _ffmpegPath,
+ Arguments = args,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false,
+ },
+ EnableRaisingEvents = true
+ };
+
+ var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+ _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
+
+ using (var processWrapper = new ProcessWrapper(process, this))
+ {
+ bool ranToCompletion = false;
+
+ await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ StartProcess(processWrapper);
+
+ // Set process priority
+ if (priority.HasValue)
+ {
+ try
+ {
+ processWrapper.Process.PriorityClass = priority.Value;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", priority.Value, processDescription);
+ }
+ }
+
+ // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
+ // but we still need to detect if the process hangs.
+ // Making the assumption that as long as new jpegs are showing up, everything is good.
+
+ bool isResponsive = true;
+ int lastCount = 0;
+ var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
+ timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
+
+ while (isResponsive)
+ {
+ try
+ {
+ await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
+
+ ranToCompletion = true;
+ break;
+ }
+ catch (OperationCanceledException)
+ {
+ // We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
+
+ isResponsive = jpegCount > lastCount;
+ lastCount = jpegCount;
+ }
+
+ if (!ranToCompletion)
+ {
+ _logger.LogInformation("Stopping trickplay extraction due to process inactivity.");
+ StopProcess(processWrapper, 1000);
+ }
+ }
+ finally
+ {
+ _thumbnailResourcePool.Release();
+ }
+
+ var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
+
+ if (exitCode == -1)
+ {
+ _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
+
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
+ }
+
+ return targetDirectory;
+ }
+ }
+
public string GetTimeParameter(long ticks)
{
var time = TimeSpan.FromTicks(ticks);
@@ -852,7 +1050,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
// We need to double escape
- return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", "'\\\\\\''", StringComparison.Ordinal);
+ return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringComparison.Ordinal);
}
/// <inheritdoc />
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index a0624fe76..a4e8194c1 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -29,8 +29,12 @@
<PackageReference Include="UTF.Unknown" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 441a3abd4..629c30060 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <summary>
/// Class responsible for normalizing FFprobe output.
/// </summary>
- public class ProbeResultNormalizer
+ public partial class ProbeResultNormalizer
{
// When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
private const int MaxSubtitleDescriptionExtractionLength = 100;
@@ -31,8 +31,6 @@ namespace MediaBrowser.MediaEncoding.Probing
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
- private static readonly Regex _performerPattern = new(@"(?<name>.*) \((?<instrument>.*)\)");
-
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
@@ -518,7 +516,7 @@ namespace MediaBrowser.MediaEncoding.Probing
private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
{
- IList<BaseItemPerson> peoples = new List<BaseItemPerson>();
+ List<BaseItemPerson> peoples = new List<BaseItemPerson>();
if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase))
{
info.Studios = pairs.Select(p => p.Value)
@@ -614,11 +612,11 @@ namespace MediaBrowser.MediaEncoding.Probing
{
codec = "dvbsub";
}
- else if ((codec ?? string.Empty).IndexOf("PGS", StringComparison.OrdinalIgnoreCase) != -1)
+ else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase))
{
codec = "PGSSUB";
}
- else if ((codec ?? string.Empty).IndexOf("DVD", StringComparison.OrdinalIgnoreCase) != -1)
+ else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase))
{
codec = "DVDSUB";
}
@@ -1184,7 +1182,7 @@ namespace MediaBrowser.MediaEncoding.Probing
info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, CultureInfo.InvariantCulture);
}
- private void SetAudioInfoFromTags(MediaInfo audio, IReadOnlyDictionary<string, string> tags)
+ private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags)
{
var people = new List<BaseItemPerson>();
if (tags.TryGetValue("composer", out var composer) && !string.IsNullOrWhiteSpace(composer))
@@ -1215,7 +1213,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(performer, false))
{
- Match match = _performerPattern.Match(person);
+ Match match = PerformerRegex().Match(person);
// If the performer doesn't have any instrument/role associated, it won't match. In that case, chances are it's simply a band name, so we skip it.
if (match.Success)
@@ -1341,7 +1339,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
// Only use the comma as a delimiter if there are no slashes or pipes.
// We want to be careful not to split names that have commas in them
- var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i, StringComparison.Ordinal) != -1) ?
+ var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.Contains(i, StringComparison.Ordinal)) ?
_nameDelimiters :
new[] { ',' };
@@ -1654,5 +1652,8 @@ namespace MediaBrowser.MediaEncoding.Probing
return TransportStreamTimestamp.Valid;
}
+
+ [GeneratedRegex("(?<name>.*) \\((?<instrument>.*)\\)")]
+ private static partial Regex PerformerRegex();
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index 0d4489517..fd55db4ba 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -88,7 +88,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public bool SupportsFileExtension(string fileExtension)
=> _subtitleFormats.ContainsKey(fileExtension);
- private IEnumerable<SubtitleFormat> GetSubtitleFormats()
+ private List<SubtitleFormat> GetSubtitleFormats()
{
var subtitleFormats = new List<SubtitleFormat>();
var assembly = typeof(SubtitleFormat).Assembly;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 21fa4468e..459d854bf 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -63,7 +63,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
- private Stream ConvertSubtitles(
+ private MemoryStream ConvertSubtitles(
Stream stream,
string inputFormat,
string outputFormat,
@@ -135,19 +135,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
- var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
+ var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
.ConfigureAwait(false);
- var inputFormat = subtitle.Format;
-
// Return the original if the same format is being requested
// Character encoding was already handled in GetSubtitleStream
if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
{
- return subtitle.Stream;
+ return stream;
}
- using (var stream = subtitle.Stream)
+ using (stream)
{
return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 3f0e98ec8..84c735f9c 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -50,6 +50,7 @@ public class EncodingOptions
EnableHardwareEncoding = true;
AllowHevcEncoding = false;
AllowAv1Encoding = false;
+ AllowMjpegEncoding = false;
EnableSubtitleExtraction = true;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
@@ -256,6 +257,11 @@ public class EncodingOptions
public bool AllowAv1Encoding { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether MJPEG encoding is enabled.
+ /// </summary>
+ public bool AllowMjpegEncoding { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether subtitle extraction is enabled.
/// </summary>
public bool EnableSubtitleExtraction { get; set; }
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 9743edb1c..fbad29143 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -35,6 +35,10 @@ namespace MediaBrowser.Model.Configuration
public bool ExtractChapterImagesDuringLibraryScan { get; set; }
+ public bool EnableTrickplayImageExtraction { get; set; }
+
+ public bool ExtractTrickplayImagesDuringLibraryScan { get; set; }
+
public MediaPathInfo[] PathInfos { get; set; }
public bool SaveLocalMetadata { get; set; }
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 78a310f0b..fe92251e9 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -4,265 +4,283 @@
using System;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.System;
using MediaBrowser.Model.Updates;
-namespace MediaBrowser.Model.Configuration
+namespace MediaBrowser.Model.Configuration;
+
+/// <summary>
+/// Represents the server configuration.
+/// </summary>
+public class ServerConfiguration : BaseApplicationConfiguration
{
/// <summary>
- /// Represents the server configuration.
+ /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
/// </summary>
- public class ServerConfiguration : BaseApplicationConfiguration
+ public ServerConfiguration()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
- /// </summary>
- public ServerConfiguration()
+ MetadataOptions = new[]
{
- MetadataOptions = new[]
+ new MetadataOptions()
+ {
+ ItemType = "Book"
+ },
+ new MetadataOptions()
+ {
+ ItemType = "Movie"
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicVideo",
+ DisabledMetadataFetchers = new[] { "The Open Movie Database" },
+ DisabledImageFetchers = new[] { "The Open Movie Database" }
+ },
+ new MetadataOptions
+ {
+ ItemType = "Series",
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicAlbum",
+ DisabledMetadataFetchers = new[] { "TheAudioDB" }
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicArtist",
+ DisabledMetadataFetchers = new[] { "TheAudioDB" }
+ },
+ new MetadataOptions
+ {
+ ItemType = "BoxSet"
+ },
+ new MetadataOptions
{
- new MetadataOptions()
- {
- ItemType = "Book"
- },
- new MetadataOptions()
- {
- ItemType = "Movie"
- },
- new MetadataOptions
- {
- ItemType = "MusicVideo",
- DisabledMetadataFetchers = new[] { "The Open Movie Database" },
- DisabledImageFetchers = new[] { "The Open Movie Database" }
- },
- new MetadataOptions
- {
- ItemType = "Series",
- },
- new MetadataOptions
- {
- ItemType = "MusicAlbum",
- DisabledMetadataFetchers = new[] { "TheAudioDB" }
- },
- new MetadataOptions
- {
- ItemType = "MusicArtist",
- DisabledMetadataFetchers = new[] { "TheAudioDB" }
- },
- new MetadataOptions
- {
- ItemType = "BoxSet"
- },
- new MetadataOptions
- {
- ItemType = "Season",
- },
- new MetadataOptions
- {
- ItemType = "Episode",
- }
- };
- }
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
- /// </summary>
- public bool EnableMetrics { get; set; } = false;
-
- public bool EnableNormalizedItemByNameIds { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is port authorized.
- /// </summary>
- /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value>
- public bool IsPortAuthorized { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether quick connect is available for use on this server.
- /// </summary>
- public bool QuickConnectAvailable { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating whether [enable case sensitive item ids].
- /// </summary>
- /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value>
- public bool EnableCaseSensitiveItemIds { get; set; } = true;
-
- public bool DisableLiveTvChannelUserDataName { get; set; } = true;
-
- /// <summary>
- /// Gets or sets the metadata path.
- /// </summary>
- /// <value>The metadata path.</value>
- public string MetadataPath { get; set; } = string.Empty;
-
- public string MetadataNetworkPath { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the preferred metadata language.
- /// </summary>
- /// <value>The preferred metadata language.</value>
- public string PreferredMetadataLanguage { get; set; } = "en";
-
- /// <summary>
- /// Gets or sets the metadata country code.
- /// </summary>
- /// <value>The metadata country code.</value>
- public string MetadataCountryCode { get; set; } = "US";
-
- /// <summary>
- /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name.
- /// </summary>
- /// <value>The sort replace characters.</value>
- public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" };
-
- /// <summary>
- /// Gets or sets characters to be removed from strings to create a sort name.
- /// </summary>
- /// <value>The sort remove characters.</value>
- public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" };
-
- /// <summary>
- /// Gets or sets words to be removed from strings to create a sort name.
- /// </summary>
- /// <value>The sort remove words.</value>
- public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" };
-
- /// <summary>
- /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
- /// </summary>
- /// <value>The min resume PCT.</value>
- public int MinResumePct { get; set; } = 5;
-
- /// <summary>
- /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
- /// </summary>
- /// <value>The max resume PCT.</value>
- public int MaxResumePct { get; set; } = 90;
-
- /// <summary>
- /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
- /// </summary>
- /// <value>The min resume duration seconds.</value>
- public int MinResumeDurationSeconds { get; set; } = 300;
-
- /// <summary>
- /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.
- /// </summary>
- /// <value>The min resume in minutes.</value>
- public int MinAudiobookResume { get; set; } = 5;
-
- /// <summary>
- /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
- /// </summary>
- /// <value>The remaining time in minutes.</value>
- public int MaxAudiobookResume { get; set; } = 5;
-
- /// <summary>
- /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
- /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
- /// different directories and files.
- /// </summary>
- /// <value>The file watcher delay.</value>
- public int LibraryMonitorDelay { get; set; } = 60;
-
- /// <summary>
- /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.
- /// </summary>
- /// <value>The library update duration.</value>
- public int LibraryUpdateDuration { get; set; } = 30;
-
- /// <summary>
- /// Gets or sets the image saving convention.
- /// </summary>
- /// <value>The image saving convention.</value>
- public ImageSavingConvention ImageSavingConvention { get; set; }
-
- public MetadataOptions[] MetadataOptions { get; set; }
-
- public bool SkipDeserializationForBasicTypes { get; set; } = true;
-
- public string ServerName { get; set; } = string.Empty;
-
- public string UICulture { get; set; } = "en-US";
-
- public bool SaveMetadataHidden { get; set; } = false;
-
- public NameValuePair[] ContentTypes { get; set; } = Array.Empty<NameValuePair>();
-
- public int RemoteClientBitrateLimit { get; set; }
-
- public bool EnableFolderView { get; set; } = false;
-
- public bool EnableGroupingIntoCollections { get; set; } = false;
-
- public bool DisplaySpecialsWithinSeasons { get; set; } = true;
-
- public string[] CodecsUsed { get; set; } = Array.Empty<string>();
-
- public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty<RepositoryInfo>();
-
- public bool EnableExternalContentInSuggestions { get; set; } = true;
-
- public int ImageExtractionTimeoutMs { get; set; }
-
- public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
-
- /// <summary>
- /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
- /// </summary>
- public bool EnableSlowResponseWarning { get; set; } = true;
-
- /// <summary>
- /// Gets or sets the threshold for the slow response time warning in ms.
- /// </summary>
- public long SlowResponseThresholdMs { get; set; } = 500;
-
- /// <summary>
- /// Gets or sets the cors hosts.
- /// </summary>
- public string[] CorsHosts { get; set; } = new[] { "*" };
-
- /// <summary>
- /// Gets or sets the number of days we should retain activity logs.
- /// </summary>
- public int? ActivityLogRetentionDays { get; set; } = 30;
+ ItemType = "Season",
+ },
+ new MetadataOptions
+ {
+ ItemType = "Episode",
+ }
+ };
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
+ /// </summary>
+ public bool EnableMetrics { get; set; } = false;
+
+ public bool EnableNormalizedItemByNameIds { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is port authorized.
+ /// </summary>
+ /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value>
+ public bool IsPortAuthorized { get; set; }
- /// <summary>
- /// Gets or sets the how the library scan fans out.
- /// </summary>
- public int LibraryScanFanoutConcurrency { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether quick connect is available for use on this server.
+ /// </summary>
+ public bool QuickConnectAvailable { get; set; } = true;
- /// <summary>
- /// Gets or sets the how many metadata refreshes can run concurrently.
- /// </summary>
- public int LibraryMetadataRefreshConcurrency { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable case sensitive item ids].
+ /// </summary>
+ /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value>
+ public bool EnableCaseSensitiveItemIds { get; set; } = true;
- /// <summary>
- /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
- /// </summary>
- public bool RemoveOldPlugins { get; set; }
+ public bool DisableLiveTvChannelUserDataName { get; set; } = true;
- /// <summary>
- /// Gets or sets a value indicating whether clients should be allowed to upload logs.
- /// </summary>
- public bool AllowClientLogUpload { get; set; } = true;
+ /// <summary>
+ /// Gets or sets the metadata path.
+ /// </summary>
+ /// <value>The metadata path.</value>
+ public string MetadataPath { get; set; } = string.Empty;
- /// <summary>
- /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
- /// </summary>
- /// <value>The dummy chapters duration.</value>
- public int DummyChapterDuration { get; set; }
+ public string MetadataNetworkPath { get; set; } = string.Empty;
- /// <summary>
- /// Gets or sets the chapter image resolution.
- /// </summary>
- /// <value>The chapter image resolution.</value>
- public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
+ /// <summary>
+ /// Gets or sets the preferred metadata language.
+ /// </summary>
+ /// <value>The preferred metadata language.</value>
+ public string PreferredMetadataLanguage { get; set; } = "en";
- /// <summary>
- /// Gets or sets the limit for parallel image encoding.
- /// </summary>
- /// <value>The limit for parallel image encoding.</value>
- public int ParallelImageEncodingLimit { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the metadata country code.
+ /// </summary>
+ /// <value>The metadata country code.</value>
+ public string MetadataCountryCode { get; set; } = "US";
+
+ /// <summary>
+ /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name.
+ /// </summary>
+ /// <value>The sort replace characters.</value>
+ public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" };
+
+ /// <summary>
+ /// Gets or sets characters to be removed from strings to create a sort name.
+ /// </summary>
+ /// <value>The sort remove characters.</value>
+ public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" };
+
+ /// <summary>
+ /// Gets or sets words to be removed from strings to create a sort name.
+ /// </summary>
+ /// <value>The sort remove words.</value>
+ public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" };
+
+ /// <summary>
+ /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
+ /// </summary>
+ /// <value>The min resume PCT.</value>
+ public int MinResumePct { get; set; } = 5;
+
+ /// <summary>
+ /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ /// </summary>
+ /// <value>The max resume PCT.</value>
+ public int MaxResumePct { get; set; } = 90;
+
+ /// <summary>
+ /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
+ /// </summary>
+ /// <value>The min resume duration seconds.</value>
+ public int MinResumeDurationSeconds { get; set; } = 300;
+
+ /// <summary>
+ /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.
+ /// </summary>
+ /// <value>The min resume in minutes.</value>
+ public int MinAudiobookResume { get; set; } = 5;
+
+ /// <summary>
+ /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ /// </summary>
+ /// <value>The remaining time in minutes.</value>
+ public int MaxAudiobookResume { get; set; } = 5;
+
+ /// <summary>
+ /// Gets or sets the threshold in minutes after a inactive session gets closed automatically.
+ /// If set to 0 the check for inactive sessions gets disabled.
+ /// </summary>
+ /// <value>The close inactive session threshold in minutes. 0 to disable.</value>
+ public int InactiveSessionThreshold { get; set; } = 10;
+
+ /// <summary>
+ /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
+ /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
+ /// different directories and files.
+ /// </summary>
+ /// <value>The file watcher delay.</value>
+ public int LibraryMonitorDelay { get; set; } = 60;
+
+ /// <summary>
+ /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.
+ /// </summary>
+ /// <value>The library update duration.</value>
+ public int LibraryUpdateDuration { get; set; } = 30;
+
+ /// <summary>
+ /// Gets or sets the image saving convention.
+ /// </summary>
+ /// <value>The image saving convention.</value>
+ public ImageSavingConvention ImageSavingConvention { get; set; }
+
+ public MetadataOptions[] MetadataOptions { get; set; }
+
+ public bool SkipDeserializationForBasicTypes { get; set; } = true;
+
+ public string ServerName { get; set; } = string.Empty;
+
+ public string UICulture { get; set; } = "en-US";
+
+ public bool SaveMetadataHidden { get; set; } = false;
+
+ public NameValuePair[] ContentTypes { get; set; } = Array.Empty<NameValuePair>();
+
+ public int RemoteClientBitrateLimit { get; set; }
+
+ public bool EnableFolderView { get; set; } = false;
+
+ public bool EnableGroupingIntoCollections { get; set; } = false;
+
+ public bool DisplaySpecialsWithinSeasons { get; set; } = true;
+
+ public string[] CodecsUsed { get; set; } = Array.Empty<string>();
+
+ public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty<RepositoryInfo>();
+
+ public bool EnableExternalContentInSuggestions { get; set; } = true;
+
+ public int ImageExtractionTimeoutMs { get; set; }
+
+ public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
+ /// </summary>
+ public bool EnableSlowResponseWarning { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the threshold for the slow response time warning in ms.
+ /// </summary>
+ public long SlowResponseThresholdMs { get; set; } = 500;
+
+ /// <summary>
+ /// Gets or sets the cors hosts.
+ /// </summary>
+ public string[] CorsHosts { get; set; } = new[] { "*" };
+
+ /// <summary>
+ /// Gets or sets the number of days we should retain activity logs.
+ /// </summary>
+ public int? ActivityLogRetentionDays { get; set; } = 30;
+
+ /// <summary>
+ /// Gets or sets the how the library scan fans out.
+ /// </summary>
+ public int LibraryScanFanoutConcurrency { get; set; }
+
+ /// <summary>
+ /// Gets or sets the how many metadata refreshes can run concurrently.
+ /// </summary>
+ public int LibraryMetadataRefreshConcurrency { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
+ /// </summary>
+ public bool RemoveOldPlugins { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether clients should be allowed to upload logs.
+ /// </summary>
+ public bool AllowClientLogUpload { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
+ /// </summary>
+ /// <value>The dummy chapters duration.</value>
+ public int DummyChapterDuration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the chapter image resolution.
+ /// </summary>
+ /// <value>The chapter image resolution.</value>
+ public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
+
+ /// <summary>
+ /// Gets or sets the limit for parallel image encoding.
+ /// </summary>
+ /// <value>The limit for parallel image encoding.</value>
+ public int ParallelImageEncodingLimit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of cast receiver applications.
+ /// </summary>
+ public CastReceiverApplication[] CastReceiverApplications { get; set; } = Array.Empty<CastReceiverApplication>();
+
+ /// <summary>
+ /// Gets or sets the trickplay options.
+ /// </summary>
+ /// <value>The trickplay options.</value>
+ public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions();
}
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
new file mode 100644
index 000000000..92c16ee84
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace MediaBrowser.Model.Configuration;
+
+/// <summary>
+/// Class TrickplayOptions.
+/// </summary>
+public class TrickplayOptions
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether or not to use HW acceleration.
+ /// </summary>
+ public bool EnableHwAcceleration { get; set; } = false;
+
+ /// <summary>
+ /// Gets or sets the behavior used by trickplay provider on library scan/update.
+ /// </summary>
+ public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
+
+ /// <summary>
+ /// Gets or sets the process priority for the ffmpeg process.
+ /// </summary>
+ public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
+
+ /// <summary>
+ /// Gets or sets the interval, in ms, between each new trickplay image.
+ /// </summary>
+ public int Interval { get; set; } = 10000;
+
+ /// <summary>
+ /// Gets or sets the target width resolutions, in px, to generates preview images for.
+ /// </summary>
+ public int[] WidthResolutions { get; set; } = new[] { 320 };
+
+ /// <summary>
+ /// Gets or sets number of tile images to allow in X dimension.
+ /// </summary>
+ public int TileWidth { get; set; } = 10;
+
+ /// <summary>
+ /// Gets or sets number of tile images to allow in Y dimension.
+ /// </summary>
+ public int TileHeight { get; set; } = 10;
+
+ /// <summary>
+ /// Gets or sets the ffmpeg output quality level.
+ /// </summary>
+ public int Qscale { get; set; } = 4;
+
+ /// <summary>
+ /// Gets or sets the jpeg quality to use for image tiles.
+ /// </summary>
+ public int JpegQuality { get; set; } = 90;
+
+ /// <summary>
+ /// Gets or sets the number of threads to be used by ffmpeg.
+ /// </summary>
+ public int ProcessThreads { get; set; } = 1;
+}
diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
new file mode 100644
index 000000000..d0db53218
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Configuration;
+
+/// <summary>
+/// Enum TrickplayScanBehavior.
+/// </summary>
+public enum TrickplayScanBehavior
+{
+ /// <summary>
+ /// Starts generation, only return once complete.
+ /// </summary>
+ Blocking,
+
+ /// <summary>
+ /// Start generation, return immediately.
+ /// </summary>
+ NonBlocking
+}
diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs
index 94f354660..b477f2593 100644
--- a/MediaBrowser.Model/Configuration/UserConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs
@@ -69,5 +69,10 @@ namespace MediaBrowser.Model.Configuration
public bool RememberSubtitleSelections { get; set; }
public bool EnableNextEpisodeAutoPlay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the selected cast receiver.
+ /// </summary>
+ public string? CastReceiverId { get; set; }
}
}
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
deleted file mode 100644
index f29022b54..000000000
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ /dev/null
@@ -1,259 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.Model.Dlna
-{
- public static class ContentFeatureBuilder
- {
- public static string BuildImageHeader(
- DeviceProfile profile,
- string container,
- int? width,
- int? height,
- bool isDirectStream,
- string orgPn = null)
- {
- string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetImageOrgOpValue();
-
- // 0 = native, 1 = transcoded
- var orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
-
- var flagValue = DlnaFlags.BackgroundTransferMode |
- DlnaFlags.InteractiveTransferMode |
- DlnaFlags.DlnaV15;
-
- string dlnaflags = string.Format(
- CultureInfo.InvariantCulture,
- ";DLNA.ORG_FLAGS={0}",
- DlnaMaps.FlagsToString(flagValue));
-
- if (string.IsNullOrEmpty(orgPn))
- {
- ResponseProfile mediaProfile = profile.GetImageMediaProfile(
- container,
- width,
- height);
-
- orgPn = mediaProfile?.OrgPn;
-
- if (string.IsNullOrEmpty(orgPn))
- {
- orgPn = GetImageOrgPnValue(container, width, height);
- }
- }
-
- if (string.IsNullOrEmpty(orgPn))
- {
- return orgOp.TrimStart(';') + orgCi + dlnaflags;
- }
-
- return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags;
- }
-
- public static string BuildAudioHeader(
- DeviceProfile profile,
- string container,
- string audioCodec,
- int? audioBitrate,
- int? audioSampleRate,
- int? audioChannels,
- int? audioBitDepth,
- bool isDirectStream,
- long? runtimeTicks,
- TranscodeSeekInfo transcodeSeekInfo)
- {
- // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none
- string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks > 0, isDirectStream, transcodeSeekInfo);
-
- // 0 = native, 1 = transcoded
- string orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
-
- var flagValue = DlnaFlags.StreamingTransferMode |
- DlnaFlags.BackgroundTransferMode |
- DlnaFlags.InteractiveTransferMode |
- DlnaFlags.DlnaV15;
-
- // if (isDirectStream)
- // {
- // flagValue = flagValue | DlnaFlags.ByteBasedSeek;
- // }
- // else if (runtimeTicks.HasValue)
- // {
- // flagValue = flagValue | DlnaFlags.TimeBasedSeek;
- // }
-
- string dlnaflags = string.Format(
- CultureInfo.InvariantCulture,
- ";DLNA.ORG_FLAGS={0}",
- DlnaMaps.FlagsToString(flagValue));
-
- ResponseProfile mediaProfile = profile.GetAudioMediaProfile(
- container,
- audioCodec,
- audioChannels,
- audioBitrate,
- audioSampleRate,
- audioBitDepth);
-
- string orgPn = mediaProfile?.OrgPn;
-
- if (string.IsNullOrEmpty(orgPn))
- {
- orgPn = GetAudioOrgPnValue(container, audioBitrate, audioSampleRate, audioChannels);
- }
-
- if (string.IsNullOrEmpty(orgPn))
- {
- return orgOp.TrimStart(';') + orgCi + dlnaflags;
- }
-
- return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags;
- }
-
- public static IEnumerable<string> BuildVideoHeader(
- DeviceProfile profile,
- string container,
- string videoCodec,
- string audioCodec,
- int? width,
- int? height,
- int? bitDepth,
- int? videoBitrate,
- TransportStreamTimestamp timestamp,
- bool isDirectStream,
- long? runtimeTicks,
- string videoProfile,
- VideoRangeType videoRangeType,
- double? videoLevel,
- float? videoFramerate,
- int? packetLength,
- TranscodeSeekInfo transcodeSeekInfo,
- bool? isAnamorphic,
- bool? isInterlaced,
- int? refFrames,
- int? numVideoStreams,
- int? numAudioStreams,
- string videoCodecTag,
- bool? isAvc)
- {
- // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none
- string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks > 0, isDirectStream, transcodeSeekInfo);
-
- // 0 = native, 1 = transcoded
- string orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
-
- var flagValue = DlnaFlags.StreamingTransferMode |
- DlnaFlags.BackgroundTransferMode |
- DlnaFlags.InteractiveTransferMode |
- DlnaFlags.DlnaV15;
-
- if (isDirectStream)
- {
- flagValue |= DlnaFlags.ByteBasedSeek;
- }
-
- // Time based seek is currently disabled when streaming. On LG CX3 adding DlnaFlags.TimeBasedSeek and orgPn causes the DLNA playback to fail (format not supported). Further investigations are needed before enabling the remaining code paths.
- // else if (runtimeTicks.HasValue)
- // {
- // flagValue = flagValue | DlnaFlags.TimeBasedSeek;
- // }
-
- string dlnaflags = string.Format(
- CultureInfo.InvariantCulture,
- ";DLNA.ORG_FLAGS={0}",
- DlnaMaps.FlagsToString(flagValue));
-
- ResponseProfile mediaProfile = profile.GetVideoMediaProfile(
- container,
- audioCodec,
- videoCodec,
- width,
- height,
- bitDepth,
- videoBitrate,
- videoProfile,
- videoRangeType,
- videoLevel,
- videoFramerate,
- packetLength,
- timestamp,
- isAnamorphic,
- isInterlaced,
- refFrames,
- numVideoStreams,
- numAudioStreams,
- videoCodecTag,
- isAvc);
-
- var orgPnValues = new List<string>();
-
- if (mediaProfile is not null && !string.IsNullOrEmpty(mediaProfile.OrgPn))
- {
- orgPnValues.AddRange(mediaProfile.OrgPn.Split(',', StringSplitOptions.RemoveEmptyEntries));
- }
- else
- {
- foreach (var s in GetVideoOrgPnValue(container, videoCodec, audioCodec, width, height, timestamp))
- {
- orgPnValues.Add(s.ToString());
- break;
- }
- }
-
- var contentFeatureList = new List<string>();
-
- foreach (string orgPn in orgPnValues)
- {
- if (string.IsNullOrEmpty(orgPn))
- {
- contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags);
- }
- else if (isDirectStream)
- {
- // orgOp should be added all the time once the time based seek is resolved for transcoded streams
- contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags);
- }
- else
- {
- contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgCi + dlnaflags);
- }
- }
-
- if (orgPnValues.Count == 0)
- {
- contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags);
- }
-
- return contentFeatureList;
- }
-
- private static string GetImageOrgPnValue(string container, int? width, int? height)
- {
- MediaFormatProfile? format = MediaFormatProfileResolver.ResolveImageFormat(container, width, height);
-
- return format.HasValue ? format.Value.ToString() : null;
- }
-
- private static string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels)
- {
- MediaFormatProfile? format = MediaFormatProfileResolver.ResolveAudioFormat(
- container,
- audioBitrate,
- audioSampleRate,
- audioChannels);
-
- return format.HasValue ? format.Value.ToString() : null;
- }
-
- private static MediaFormatProfile[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp)
- {
- return MediaFormatProfileResolver.ResolveVideoFormat(container, videoCodec, audioCodec, width, height, timestamp);
- }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 07bb002ea..71d0896a7 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -1,6 +1,7 @@
#pragma warning disable CA1819 // Properties should not return arrays
using System;
using System.ComponentModel;
+using System.Linq;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -227,9 +228,12 @@ namespace MediaBrowser.Model.Dlna
/// The GetSupportedMediaTypes.
/// </summary>
/// <returns>The .</returns>
- public string[] GetSupportedMediaTypes()
+ public MediaType[] GetSupportedMediaTypes()
{
- return ContainerProfile.SplitValue(SupportedMediaTypes);
+ return ContainerProfile.SplitValue(SupportedMediaTypes)
+ .Select(m => Enum.TryParse<MediaType>(m, out var parsed) ? parsed : MediaType.Unknown)
+ .Where(m => m != MediaType.Unknown)
+ .ToArray();
}
/// <summary>
diff --git a/MediaBrowser.Model/Dlna/DlnaFlags.cs b/MediaBrowser.Model/Dlna/DlnaFlags.cs
deleted file mode 100644
index 02d9ea9c5..000000000
--- a/MediaBrowser.Model/Dlna/DlnaFlags.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Dlna
-{
- [Flags]
- public enum DlnaFlags : ulong
- {
- /*! <i>Background</i> transfer mode.
- For use with upload and download transfers to and from the server.
- The primary difference between \ref DH_TransferMode_Interactive and
- \ref DH_TransferMode_Bulk is that the latter assumes that the user
- is not relying on the transfer for immediately rendering the content
- and there are no issues with causing a buffer overflow if the
- receiver uses TCP flow control to reduce total throughput.
- */
- BackgroundTransferMode = 1 << 22,
-
- ByteBasedSeek = 1 << 29,
- ConnectionStall = 1 << 21,
-
- DlnaV15 = 1 << 20,
-
- /*! <i>Interactive</i> transfer mode.
- For best effort transfer of images and non-real-time transfers.
- URIs with image content usually support \ref DH_TransferMode_Bulk too.
- The primary difference between \ref DH_TransferMode_Interactive and
- \ref DH_TransferMode_Bulk is that the former assumes that the
- transfer is intended for immediate rendering.
- */
- InteractiveTransferMode = 1 << 23,
-
- PlayContainer = 1 << 28,
- RtspPause = 1 << 25,
- S0Increase = 1 << 27,
- SenderPaced = 1L << 31,
- SnIncrease = 1 << 26,
-
- /*! <i>Streaming</i> transfer mode.
- The server transmits at a throughput sufficient for real-time playback of
- audio or video. URIs with audio or video often support the
- \ref DH_TransferMode_Interactive and \ref DH_TransferMode_Bulk transfer modes.
- The most well-known exception to this general claim is for live streams.
- */
- StreamingTransferMode = 1 << 24,
-
- TimeBasedSeek = 1 << 30
- }
-}
diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs
deleted file mode 100644
index 4613bc542..000000000
--- a/MediaBrowser.Model/Dlna/DlnaMaps.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Globalization;
-
-namespace MediaBrowser.Model.Dlna
-{
- public static class DlnaMaps
- {
- public static string FlagsToString(DlnaFlags flags)
- {
- return string.Format(CultureInfo.InvariantCulture, "{0:X8}{1:D24}", (ulong)flags, 0);
- }
-
- public static string GetOrgOpValue(bool hasKnownRuntime, bool isDirectStream, TranscodeSeekInfo profileTranscodeSeekInfo)
- {
- if (hasKnownRuntime)
- {
- string orgOp = string.Empty;
-
- // Time-based seeking currently only possible when transcoding
- orgOp += isDirectStream ? "0" : "1";
-
- // Byte-based seeking only possible when not transcoding
- orgOp += isDirectStream || profileTranscodeSeekInfo == TranscodeSeekInfo.Bytes ? "1" : "0";
-
- return orgOp;
- }
-
- // No seeking is available if we don't know the content runtime
- return "00";
- }
-
- public static string GetImageOrgOpValue()
- {
- string orgOp = string.Empty;
-
- // Time-based seeking currently only possible when transcoding
- orgOp += "0";
-
- // Byte-based seeking only possible when not transcoding
- orgOp += "0";
-
- return orgOp;
- }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
deleted file mode 100644
index 086088dea..000000000
--- a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using Jellyfin.Data.Events;
-
-namespace MediaBrowser.Model.Dlna
-{
- public interface IDeviceDiscovery
- {
- event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered;
-
- event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
- }
-}
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs
deleted file mode 100644
index 06f6660f4..000000000
--- a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-#pragma warning disable CS1591, CA1707
-
-namespace MediaBrowser.Model.Dlna
-{
- public enum MediaFormatProfile
- {
- MP3,
- WMA_BASE,
- WMA_FULL,
- LPCM16_44_MONO,
- LPCM16_44_STEREO,
- LPCM16_48_MONO,
- LPCM16_48_STEREO,
- AAC_ISO,
- AAC_ISO_320,
- AAC_ADTS,
- AAC_ADTS_320,
- FLAC,
- OGG,
-
- JPEG_SM,
- JPEG_MED,
- JPEG_LRG,
- JPEG_TN,
- PNG_LRG,
- PNG_TN,
- GIF_LRG,
- RAW,
-
- MPEG1,
- MPEG_PS_PAL,
- MPEG_PS_NTSC,
- MPEG_TS_SD_EU,
- MPEG_TS_SD_EU_ISO,
- MPEG_TS_SD_EU_T,
- MPEG_TS_SD_NA,
- MPEG_TS_SD_NA_ISO,
- MPEG_TS_SD_NA_T,
- MPEG_TS_SD_KO,
- MPEG_TS_SD_KO_ISO,
- MPEG_TS_SD_KO_T,
- MPEG_TS_JP_T,
- AVI,
- MATROSKA,
- FLV,
- DVR_MS,
- WTV,
- OGV,
- AVC_MP4_MP_SD_AAC_MULT5,
- AVC_MP4_MP_SD_MPEG1_L3,
- AVC_MP4_MP_SD_AC3,
- AVC_MP4_MP_HD_720p_AAC,
- AVC_MP4_MP_HD_1080i_AAC,
- AVC_MP4_HP_HD_AAC,
- AVC_TS_MP_HD_AAC_MULT5,
- AVC_TS_MP_HD_AAC_MULT5_T,
- AVC_TS_MP_HD_AAC_MULT5_ISO,
- AVC_TS_MP_HD_MPEG1_L3,
- AVC_TS_MP_HD_MPEG1_L3_T,
- AVC_TS_MP_HD_MPEG1_L3_ISO,
- AVC_TS_MP_HD_AC3,
- AVC_TS_MP_HD_AC3_T,
- AVC_TS_MP_HD_AC3_ISO,
- AVC_TS_HP_HD_MPEG1_L2_T,
- AVC_TS_HP_HD_MPEG1_L2_ISO,
- AVC_TS_MP_SD_AAC_MULT5,
- AVC_TS_MP_SD_AAC_MULT5_T,
- AVC_TS_MP_SD_AAC_MULT5_ISO,
- AVC_TS_MP_SD_MPEG1_L3,
- AVC_TS_MP_SD_MPEG1_L3_T,
- AVC_TS_MP_SD_MPEG1_L3_ISO,
- AVC_TS_HP_SD_MPEG1_L2_T,
- AVC_TS_HP_SD_MPEG1_L2_ISO,
- AVC_TS_MP_SD_AC3,
- AVC_TS_MP_SD_AC3_T,
- AVC_TS_MP_SD_AC3_ISO,
- AVC_TS_HD_DTS_T,
- AVC_TS_HD_DTS_ISO,
- WMVMED_BASE,
- WMVMED_FULL,
- WMVMED_PRO,
- WMVHIGH_FULL,
- WMVHIGH_PRO,
- VC1_ASF_AP_L1_WMA,
- VC1_ASF_AP_L2_WMA,
- VC1_ASF_AP_L3_WMA,
- VC1_TS_AP_L1_AC3_ISO,
- VC1_TS_AP_L2_AC3_ISO,
- VC1_TS_HD_DTS_ISO,
- VC1_TS_HD_DTS_T,
- MPEG4_P2_MP4_ASP_AAC,
- MPEG4_P2_MP4_SP_L6_AAC,
- MPEG4_P2_MP4_NDSD,
- MPEG4_P2_TS_ASP_AAC,
- MPEG4_P2_TS_ASP_AAC_T,
- MPEG4_P2_TS_ASP_AAC_ISO,
- MPEG4_P2_TS_ASP_MPEG1_L3,
- MPEG4_P2_TS_ASP_MPEG1_L3_T,
- MPEG4_P2_TS_ASP_MPEG1_L3_ISO,
- MPEG4_P2_TS_ASP_MPEG2_L2,
- MPEG4_P2_TS_ASP_MPEG2_L2_T,
- MPEG4_P2_TS_ASP_MPEG2_L2_ISO,
- MPEG4_P2_TS_ASP_AC3,
- MPEG4_P2_TS_ASP_AC3_T,
- MPEG4_P2_TS_ASP_AC3_ISO,
- AVC_TS_HD_50_LPCM_T,
- AVC_MP4_LPCM,
- MPEG4_P2_3GPP_SP_L0B_AAC,
- MPEG4_P2_3GPP_SP_L0B_AMR,
- AVC_3GPP_BL_QCIF15_AAC,
- MPEG4_H263_3GPP_P0_L10_AMR,
- MPEG4_H263_MP4_P0_L10_AAC
- }
-}
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
deleted file mode 100644
index 93a9ae615..000000000
--- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
+++ /dev/null
@@ -1,532 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.Model.Dlna
-{
- public static class MediaFormatProfileResolver
- {
- public static MediaFormatProfile[] ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
- {
- if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase))
- {
- MediaFormatProfile? val = ResolveVideoASFFormat(videoCodec, audioCodec, width, height);
- return val.HasValue ? new MediaFormatProfile[] { val.Value } : Array.Empty<MediaFormatProfile>();
- }
-
- if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase))
- {
- MediaFormatProfile? val = ResolveVideoMP4Format(videoCodec, audioCodec, width, height);
- return val.HasValue ? new MediaFormatProfile[] { val.Value } : Array.Empty<MediaFormatProfile>();
- }
-
- if (string.Equals(container, "avi", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.AVI };
- }
-
- if (string.Equals(container, "mkv", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.MATROSKA };
- }
-
- if (string.Equals(container, "mpeg2ps", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.MPEG_PS_NTSC, MediaFormatProfile.MPEG_PS_PAL };
- }
-
- if (string.Equals(container, "mpeg1video", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.MPEG1 };
- }
-
- if (string.Equals(container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase))
- {
- return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType);
- }
-
- if (string.Equals(container, "flv", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.FLV };
- }
-
- if (string.Equals(container, "wtv", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.WTV };
- }
-
- if (string.Equals(container, "3gp", StringComparison.OrdinalIgnoreCase))
- {
- MediaFormatProfile? val = ResolveVideo3GPFormat(videoCodec, audioCodec);
- return val.HasValue ? new MediaFormatProfile[] { val.Value } : Array.Empty<MediaFormatProfile>();
- }
-
- if (string.Equals(container, "ogv", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.OGV };
- }
-
- return Array.Empty<MediaFormatProfile>();
- }
-
- private static MediaFormatProfile[] ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
- {
- string suffix = string.Empty;
-
- switch (timestampType)
- {
- case TransportStreamTimestamp.None:
- suffix = "_ISO";
- break;
- case TransportStreamTimestamp.Valid:
- suffix = "_T";
- break;
- }
-
- string resolution = "S";
- if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576))
- {
- resolution = "H";
- }
-
- if (string.Equals(videoCodec, "mpeg2video", StringComparison.OrdinalIgnoreCase))
- {
- var list = new List<MediaFormatProfile>
- {
- ValueOf("MPEG_TS_SD_NA" + suffix),
- ValueOf("MPEG_TS_SD_EU" + suffix),
- ValueOf("MPEG_TS_SD_KO" + suffix)
- };
-
- if ((timestampType == TransportStreamTimestamp.Valid) && string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- list.Add(MediaFormatProfile.MPEG_TS_JP_T);
- }
-
- return list.ToArray();
- }
-
- if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- if (string.Equals(audioCodec, "lpcm", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_50_LPCM_T };
- }
-
- if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase))
- {
- if (timestampType == TransportStreamTimestamp.None)
- {
- return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_ISO };
- }
-
- return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_T };
- }
-
- if (string.Equals(audioCodec, "mp2", StringComparison.OrdinalIgnoreCase))
- {
- if (timestampType == TransportStreamTimestamp.None)
- {
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_HP_{0}D_MPEG1_L2_ISO", resolution)) };
- }
-
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) };
- }
-
- if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) };
- }
-
- if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) };
- }
-
- if (string.IsNullOrEmpty(audioCodec) ||
- string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) };
- }
- }
- else if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase))
- {
- if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576))
- {
- return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L2_AC3_ISO };
- }
-
- return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L1_AC3_ISO };
- }
-
- if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase))
- {
- suffix = string.Equals(suffix, "_ISO", StringComparison.OrdinalIgnoreCase) ? suffix : "_T";
-
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "VC1_TS_HD_DTS{0}", suffix)) };
- }
- }
- else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_AAC{0}", suffix)) };
- }
-
- if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) };
- }
-
- if (string.Equals(audioCodec, "mp2", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) };
- }
-
- if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_AC3{0}", suffix)) };
- }
- }
-
- return Array.Empty<MediaFormatProfile>();
- }
-
- private static MediaFormatProfile ValueOf(string value)
- {
- return (MediaFormatProfile)Enum.Parse(typeof(MediaFormatProfile), value, true);
- }
-
- private static MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height)
- {
- if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- if (string.Equals(audioCodec, "lpcm", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.AVC_MP4_LPCM;
- }
-
- if (string.IsNullOrEmpty(audioCodec) ||
- string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.AVC_MP4_MP_SD_AC3;
- }
-
- if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.AVC_MP4_MP_SD_MPEG1_L3;
- }
-
- if (width.HasValue && height.HasValue)
- {
- if ((width.Value <= 720) && (height.Value <= 576))
- {
- if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.AVC_MP4_MP_SD_AAC_MULT5;
- }
- }
- else if ((width.Value <= 1280) && (height.Value <= 720))
- {
- if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.AVC_MP4_MP_HD_720p_AAC;
- }
- }
- else if ((width.Value <= 1920) && (height.Value <= 1080))
- {
- if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.AVC_MP4_MP_HD_1080i_AAC;
- }
- }
- }
- }
- else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- if (width.HasValue && height.HasValue && width.Value <= 720 && height.Value <= 576)
- {
- if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.MPEG4_P2_MP4_ASP_AAC;
- }
-
- if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.MPEG4_P2_MP4_NDSD;
- }
- }
- else if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.MPEG4_P2_MP4_SP_L6_AAC;
- }
- }
- else if (string.Equals(videoCodec, "h263", StringComparison.OrdinalIgnoreCase) && string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.MPEG4_H263_MP4_P0_L10_AAC;
- }
-
- return null;
- }
-
- private static MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec)
- {
- if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.AVC_3GPP_BL_QCIF15_AAC;
- }
- }
- else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AAC;
- }
-
- if (string.Equals(audioCodec, "amrnb", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AMR;
- }
- }
- else if (string.Equals(videoCodec, "h263", StringComparison.OrdinalIgnoreCase) && string.Equals(audioCodec, "amrnb", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.MPEG4_H263_3GPP_P0_L10_AMR;
- }
-
- return null;
- }
-
- private static MediaFormatProfile? ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height)
- {
- if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) &&
- (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase)))
- {
- if (width.HasValue && height.HasValue)
- {
- if ((width.Value <= 720) && (height.Value <= 576))
- {
- if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.WMVMED_FULL;
- }
-
- return MediaFormatProfile.WMVMED_PRO;
- }
- }
-
- if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.WMVHIGH_FULL;
- }
-
- return MediaFormatProfile.WMVHIGH_PRO;
- }
-
- if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase))
- {
- if (width.HasValue && height.HasValue)
- {
- if ((width.Value <= 720) && (height.Value <= 576))
- {
- return MediaFormatProfile.VC1_ASF_AP_L1_WMA;
- }
-
- if ((width.Value <= 1280) && (height.Value <= 720))
- {
- return MediaFormatProfile.VC1_ASF_AP_L2_WMA;
- }
-
- if ((width.Value <= 1920) && (height.Value <= 1080))
- {
- return MediaFormatProfile.VC1_ASF_AP_L3_WMA;
- }
- }
- }
- else if (string.Equals(videoCodec, "mpeg2video", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.DVR_MS;
- }
-
- return null;
- }
-
- public static MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels)
- {
- if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase))
- {
- return ResolveAudioASFFormat(bitrate);
- }
-
- if (string.Equals(container, "mp3", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.MP3;
- }
-
- if (string.Equals(container, "lpcm", StringComparison.OrdinalIgnoreCase))
- {
- return ResolveAudioLPCMFormat(frequency, channels);
- }
-
- if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(container, "aac", StringComparison.OrdinalIgnoreCase))
- {
- return ResolveAudioMP4Format(bitrate);
- }
-
- if (string.Equals(container, "adts", StringComparison.OrdinalIgnoreCase))
- {
- return ResolveAudioADTSFormat(bitrate);
- }
-
- if (string.Equals(container, "flac", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.FLAC;
- }
-
- if (string.Equals(container, "oga", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.OGG;
- }
-
- return null;
- }
-
- private static MediaFormatProfile ResolveAudioASFFormat(int? bitrate)
- {
- if (bitrate.HasValue && bitrate.Value <= 193)
- {
- return MediaFormatProfile.WMA_BASE;
- }
-
- return MediaFormatProfile.WMA_FULL;
- }
-
- private static MediaFormatProfile? ResolveAudioLPCMFormat(int? frequency, int? channels)
- {
- if (frequency.HasValue && channels.HasValue)
- {
- if (frequency.Value == 44100 && channels.Value == 1)
- {
- return MediaFormatProfile.LPCM16_44_MONO;
- }
-
- if (frequency.Value == 44100 && channels.Value == 2)
- {
- return MediaFormatProfile.LPCM16_44_STEREO;
- }
-
- if (frequency.Value == 48000 && channels.Value == 1)
- {
- return MediaFormatProfile.LPCM16_48_MONO;
- }
-
- if (frequency.Value == 48000 && channels.Value == 2)
- {
- return MediaFormatProfile.LPCM16_48_STEREO;
- }
-
- return null;
- }
-
- return MediaFormatProfile.LPCM16_48_STEREO;
- }
-
- private static MediaFormatProfile ResolveAudioMP4Format(int? bitrate)
- {
- if (bitrate.HasValue && bitrate.Value <= 320)
- {
- return MediaFormatProfile.AAC_ISO_320;
- }
-
- return MediaFormatProfile.AAC_ISO;
- }
-
- private static MediaFormatProfile ResolveAudioADTSFormat(int? bitrate)
- {
- if (bitrate.HasValue && bitrate.Value <= 320)
- {
- return MediaFormatProfile.AAC_ADTS_320;
- }
-
- return MediaFormatProfile.AAC_ADTS;
- }
-
- public static MediaFormatProfile? ResolveImageFormat(string container, int? width, int? height)
- {
- if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase))
- {
- return ResolveImageJPGFormat(width, height);
- }
-
- if (string.Equals(container, "png", StringComparison.OrdinalIgnoreCase))
- {
- return ResolveImagePNGFormat(width, height);
- }
-
- if (string.Equals(container, "gif", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.GIF_LRG;
- }
-
- if (string.Equals(container, "raw", StringComparison.OrdinalIgnoreCase))
- {
- return MediaFormatProfile.RAW;
- }
-
- return null;
- }
-
- private static MediaFormatProfile ResolveImageJPGFormat(int? width, int? height)
- {
- if (width.HasValue && height.HasValue)
- {
- if ((width.Value <= 160) && (height.Value <= 160))
- {
- return MediaFormatProfile.JPEG_TN;
- }
-
- if ((width.Value <= 640) && (height.Value <= 480))
- {
- return MediaFormatProfile.JPEG_SM;
- }
-
- if ((width.Value <= 1024) && (height.Value <= 768))
- {
- return MediaFormatProfile.JPEG_MED;
- }
-
- return MediaFormatProfile.JPEG_LRG;
- }
-
- return MediaFormatProfile.JPEG_SM;
- }
-
- private static MediaFormatProfile ResolveImagePNGFormat(int? width, int? height)
- {
- if (width.HasValue && height.HasValue)
- {
- if ((width.Value <= 160) && (height.Value <= 160))
- {
- return MediaFormatProfile.PNG_TN;
- }
- }
-
- return MediaFormatProfile.PNG_LRG;
- }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs
deleted file mode 100644
index 6f4a692c8..000000000
--- a/MediaBrowser.Model/Dlna/SearchCriteria.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Text.RegularExpressions;
-
-namespace MediaBrowser.Model.Dlna
-{
- public partial class SearchCriteria
- {
- public SearchCriteria(string search)
- {
- ArgumentException.ThrowIfNullOrEmpty(search);
-
- SearchType = SearchType.Unknown;
-
- string[] factors = AndOrRegex().Split(search);
- foreach (string factor in factors)
- {
- string[] subFactors = WhiteSpaceRegex().Split(factor.Trim().Trim('(').Trim(')').Trim(), 3);
-
- if (subFactors.Length == 3)
- {
- if (string.Equals("upnp:class", subFactors[0], StringComparison.OrdinalIgnoreCase)
- && (string.Equals("=", subFactors[1], StringComparison.Ordinal) || string.Equals("derivedfrom", subFactors[1], StringComparison.OrdinalIgnoreCase)))
- {
- if (string.Equals("\"object.item.imageItem\"", subFactors[2], StringComparison.Ordinal) || string.Equals("\"object.item.imageItem.photo\"", subFactors[2], StringComparison.OrdinalIgnoreCase))
- {
- SearchType = SearchType.Image;
- }
- else if (string.Equals("\"object.item.videoItem\"", subFactors[2], StringComparison.OrdinalIgnoreCase))
- {
- SearchType = SearchType.Video;
- }
- else if (string.Equals("\"object.container.playlistContainer\"", subFactors[2], StringComparison.OrdinalIgnoreCase))
- {
- SearchType = SearchType.Playlist;
- }
- else if (string.Equals("\"object.container.album.musicAlbum\"", subFactors[2], StringComparison.OrdinalIgnoreCase))
- {
- SearchType = SearchType.MusicAlbum;
- }
- }
- }
- }
- }
-
- public SearchType SearchType { get; set; }
-
- [GeneratedRegex("\\s")]
- private static partial Regex WhiteSpaceRegex();
-
- [GeneratedRegex("(and|or)", RegexOptions.IgnoreCase)]
- private static partial Regex AndOrRegex();
- }
-}
diff --git a/MediaBrowser.Model/Dlna/SearchType.cs b/MediaBrowser.Model/Dlna/SearchType.cs
deleted file mode 100644
index 8bc7c5249..000000000
--- a/MediaBrowser.Model/Dlna/SearchType.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Dlna
-{
- public enum SearchType
- {
- Unknown = 0,
- Audio = 1,
- Image = 2,
- Video = 3,
- Playlist = 4,
- MusicAlbum = 5
- }
-}
diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs
deleted file mode 100644
index 7df53c6d1..000000000
--- a/MediaBrowser.Model/Dlna/SortCriteria.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using Jellyfin.Data.Enums;
-
-namespace MediaBrowser.Model.Dlna
-{
- public class SortCriteria
- {
- public SortCriteria(string sortOrder)
- {
- if (Enum.TryParse<SortOrder>(sortOrder, true, out var sortOrderValue))
- {
- SortOrder = sortOrderValue;
- }
- else
- {
- SortOrder = SortOrder.Ascending;
- }
- }
-
- public SortOrder SortOrder { get; }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 889e2494a..bf18d46dc 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -835,11 +835,6 @@ namespace MediaBrowser.Model.Dlna
playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant());
}
- if (videoStream is not null && videoStream.Level != 0)
- {
- playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString() ?? string.Empty);
- }
-
// Prefer matching audio codecs, could do better here
var audioCodecs = ContainerProfile.SplitValue(audioCodec);
@@ -866,16 +861,16 @@ namespace MediaBrowser.Model.Dlna
// Copy matching audio codec options
playlistItem.AudioSampleRate = audioStream.SampleRate;
- playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString() ?? string.Empty);
+ playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels?.ToString(CultureInfo.InvariantCulture) ?? string.Empty);
if (!string.IsNullOrEmpty(audioStream.Profile))
{
playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant());
}
- if (audioStream.Level != 0)
+ if (audioStream.Level.HasValue && audioStream.Level.Value != 0)
{
- playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString() ?? string.Empty);
+ playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.Value.ToString(CultureInfo.InvariantCulture));
}
}
@@ -1313,7 +1308,7 @@ namespace MediaBrowser.Model.Dlna
var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions);
- if (audioStream?.IsExternal == true)
+ if (audioStream.IsExternal == true)
{
audioStreamFailureReasons |= TranscodeReason.AudioIsExternal;
}
diff --git a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs
deleted file mode 100644
index c7489d57a..000000000
--- a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net;
-
-namespace MediaBrowser.Model.Dlna
-{
- public class UpnpDeviceInfo
- {
- public Uri Location { get; set; }
-
- public Dictionary<string, string> Headers { get; set; }
-
- public IPAddress LocalIPAddress { get; set; }
-
- public int LocalPort { get; set; }
-
- public IPAddress RemoteIPAddress { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 8fab1ca6d..d257eab92 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
@@ -419,7 +420,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the type of the collection.
/// </summary>
/// <value>The type of the collection.</value>
- public string CollectionType { get; set; }
+ public CollectionType? CollectionType { get; set; }
/// <summary>
/// Gets or sets the display order.
@@ -569,6 +570,12 @@ namespace MediaBrowser.Model.Dto
public List<ChapterInfo> Chapters { get; set; }
/// <summary>
+ /// Gets or sets the trickplay manifest.
+ /// </summary>
+ /// <value>The trickplay manifest.</value>
+ public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; }
+
+ /// <summary>
/// Gets or sets the type of the location.
/// </summary>
/// <value>The type of the location.</value>
@@ -584,7 +591,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the type of the media.
/// </summary>
/// <value>The type of the media.</value>
- public string MediaType { get; set; }
+ public MediaType MediaType { get; set; }
/// <summary>
/// Gets or sets the end date.
diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
index d098669ba..a3035bf61 100644
--- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
+++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Providers;
@@ -27,7 +28,7 @@ namespace MediaBrowser.Model.Dto
public IReadOnlyList<ExternalIdInfo> ExternalIdInfos { get; set; }
- public string? ContentType { get; set; }
+ public CollectionType? ContentType { get; set; }
public IReadOnlyList<NameValuePair> ContentTypeOptions { get; set; }
}
diff --git a/MediaBrowser.Model/Dto/UpdateUserItemDataDto.cs b/MediaBrowser.Model/Dto/UpdateUserItemDataDto.cs
new file mode 100644
index 000000000..7bfedf973
--- /dev/null
+++ b/MediaBrowser.Model/Dto/UpdateUserItemDataDto.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// This is used by the api to get information about a item user data.
+ /// </summary>
+ public class UpdateUserItemDataDto
+ {
+ /// <summary>
+ /// Gets or sets the rating.
+ /// </summary>
+ /// <value>The rating.</value>
+ public double? Rating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the played percentage.
+ /// </summary>
+ /// <value>The played percentage.</value>
+ public double? PlayedPercentage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the unplayed item count.
+ /// </summary>
+ /// <value>The unplayed item count.</value>
+ public int? UnplayedItemCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playback position ticks.
+ /// </summary>
+ /// <value>The playback position ticks.</value>
+ public long? PlaybackPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play count.
+ /// </summary>
+ /// <value>The play count.</value>
+ public int? PlayCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
+ public bool? IsFavorite { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="UpdateUserItemDataDto" /> is likes.
+ /// </summary>
+ /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
+ public bool? Likes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last played date.
+ /// </summary>
+ /// <value>The last played date.</value>
+ public DateTime? LastPlayedDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="UserItemDataDto" /> is played.
+ /// </summary>
+ /// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
+ public bool? Played { get; set; }
+
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public string? Key { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item identifier.
+ /// </summary>
+ /// <value>The item identifier.</value>
+ public string? ItemId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/CollectionType.cs b/MediaBrowser.Model/Entities/CollectionType.cs
deleted file mode 100644
index 60b69d4b0..000000000
--- a/MediaBrowser.Model/Entities/CollectionType.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Entities
-{
- public static class CollectionType
- {
- public const string Movies = "movies";
-
- public const string TvShows = "tvshows";
-
- public const string Music = "music";
-
- public const string MusicVideos = "musicvideos";
-
- public const string Trailers = "trailers";
-
- public const string HomeVideos = "homevideos";
-
- public const string BoxSets = "boxsets";
-
- public const string Books = "books";
- public const string Photos = "photos";
- public const string LiveTv = "livetv";
- public const string Playlists = "playlists";
- public const string Folders = "folders";
- }
-}
diff --git a/MediaBrowser.Model/Entities/MediaType.cs b/MediaBrowser.Model/Entities/MediaType.cs
deleted file mode 100644
index dd2ae810b..000000000
--- a/MediaBrowser.Model/Entities/MediaType.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace MediaBrowser.Model.Entities
-{
- /// <summary>
- /// Class MediaType.
- /// </summary>
- public static class MediaType
- {
- /// <summary>
- /// The video.
- /// </summary>
- public const string Video = "Video";
-
- /// <summary>
- /// The audio.
- /// </summary>
- public const string Audio = "Audio";
-
- /// <summary>
- /// The photo.
- /// </summary>
- public const string Photo = "Photo";
-
- /// <summary>
- /// The book.
- /// </summary>
- public const string Book = "Book";
- }
-}
diff --git a/MediaBrowser.Model/Entities/UserDataSaveReason.cs b/MediaBrowser.Model/Entities/UserDataSaveReason.cs
index 20404e6f4..b8e73a98c 100644
--- a/MediaBrowser.Model/Entities/UserDataSaveReason.cs
+++ b/MediaBrowser.Model/Entities/UserDataSaveReason.cs
@@ -33,6 +33,11 @@ namespace MediaBrowser.Model.Entities
/// <summary>
/// The import.
/// </summary>
- Import = 6
+ Import = 6,
+
+ /// <summary>
+ /// API call updated item user data.
+ /// </summary>
+ UpdateUserData = 7,
}
}
diff --git a/MediaBrowser.Model/Library/UserViewQuery.cs b/MediaBrowser.Model/Library/UserViewQuery.cs
index 8a49b6863..e20d6af49 100644
--- a/MediaBrowser.Model/Library/UserViewQuery.cs
+++ b/MediaBrowser.Model/Library/UserViewQuery.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Library
{
@@ -9,7 +10,7 @@ namespace MediaBrowser.Model.Library
public UserViewQuery()
{
IncludeExternalContent = true;
- PresetViews = Array.Empty<string>();
+ PresetViews = Array.Empty<CollectionType?>();
}
/// <summary>
@@ -30,6 +31,6 @@ namespace MediaBrowser.Model.Library
/// <value><c>true</c> if [include hidden]; otherwise, <c>false</c>.</value>
public bool IncludeHidden { get; set; }
- public string[] PresetViews { get; set; }
+ public CollectionType?[] PresetViews { get; set; }
}
}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
index 673d97a9e..d872572b7 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
@@ -14,7 +14,7 @@ namespace MediaBrowser.Model.LiveTv
public LiveTvChannelQuery()
{
EnableUserData = true;
- SortBy = Array.Empty<string>();
+ SortBy = Array.Empty<ItemSortBy>();
}
/// <summary>
@@ -99,7 +99,7 @@ namespace MediaBrowser.Model.LiveTv
public bool? IsSeries { get; set; }
- public string[] SortBy { get; set; }
+ public ItemSortBy[] SortBy { get; set; }
/// <summary>
/// Gets or sets the sort order to return results with.
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 58ba83a35..7af46f8a0 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -14,7 +14,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -34,7 +34,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="MimeTypes">
<PrivateAssets>all</PrivateAssets>
@@ -48,8 +47,12 @@
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@@ -58,6 +61,7 @@
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
+
<ItemGroup>
<ProjectReference Include="../Jellyfin.Data/Jellyfin.Data.csproj" />
<ProjectReference Include="../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" />
diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs
index e9fcd6797..c116d883e 100644
--- a/MediaBrowser.Model/Net/IPData.cs
+++ b/MediaBrowser.Model/Net/IPData.cs
@@ -1,6 +1,6 @@
using System.Net;
using System.Net.Sockets;
-using Microsoft.AspNetCore.HttpOverrides;
+using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
namespace MediaBrowser.Model.Net;
diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs
index 128034eb8..62b87d9f5 100644
--- a/MediaBrowser.Model/Net/ISocketFactory.cs
+++ b/MediaBrowser.Model/Net/ISocketFactory.cs
@@ -14,22 +14,4 @@ public interface ISocketFactory
/// <param name="localPort">The local port to bind to.</param>
/// <returns>A new unicast socket using the specified local port number.</returns>
Socket CreateUdpBroadcastSocket(int localPort);
-
- /// <summary>
- /// Creates a new unicast socket using the specified local port number.
- /// </summary>
- /// <param name="bindInterface">The bind interface.</param>
- /// <param name="localPort">The local port to bind to.</param>
- /// <returns>A new unicast socket using the specified local port number.</returns>
- Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort);
-
- /// <summary>
- /// Creates a new multicast socket using the specified multicast IP address, multicast time to live and local port.
- /// </summary>
- /// <param name="multicastAddress">The multicast IP address to bind to.</param>
- /// <param name="bindInterface">The bind interface.</param>
- /// <param name="multicastTimeToLive">The multicast time to live value. Actually a maximum number of network hops for UDP packets.</param>
- /// <param name="localPort">The local port to bind to.</param>
- /// <returns>A new multicast socket using the specfied bind interface, multicast address, multicast time to live and port.</returns>
- Socket CreateUdpMulticastSocket(IPAddress multicastAddress, IPData bindInterface, int multicastTimeToLive, int localPort);
}
diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
index 847269716..62d496d04 100644
--- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
+++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Playlists;
@@ -22,7 +23,7 @@ public class PlaylistCreationRequest
/// <summary>
/// Gets or sets the media type.
/// </summary>
- public string? MediaType { get; set; }
+ public MediaType? MediaType { get; set; }
/// <summary>
/// Gets or sets the user id.
diff --git a/MediaBrowser.Model/Providers/ExternalIdMediaType.cs b/MediaBrowser.Model/Providers/ExternalIdMediaType.cs
index 5303c8f58..ef518369c 100644
--- a/MediaBrowser.Model/Providers/ExternalIdMediaType.cs
+++ b/MediaBrowser.Model/Providers/ExternalIdMediaType.cs
@@ -66,6 +66,11 @@ namespace MediaBrowser.Model.Providers
/// <summary>
/// A music track.
/// </summary>
- Track = 12
+ Track = 12,
+
+ /// <summary>
+ /// A book.
+ /// </summary>
+ Book = 13
}
}
diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs
index 6fa1d778a..242a1c6e9 100644
--- a/MediaBrowser.Model/Querying/ItemFields.cs
+++ b/MediaBrowser.Model/Querying/ItemFields.cs
@@ -34,6 +34,11 @@ namespace MediaBrowser.Model.Querying
/// </summary>
Chapters,
+ /// <summary>
+ /// The trickplay manifest.
+ /// </summary>
+ Trickplay,
+
ChildCount,
/// <summary>
diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs
deleted file mode 100644
index 1a7c9a63b..000000000
--- a/MediaBrowser.Model/Querying/ItemSortBy.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-namespace MediaBrowser.Model.Querying
-{
- /// <summary>
- /// These represent sort orders that are known by the core.
- /// </summary>
- public static class ItemSortBy
- {
- /// <summary>
- /// The aired episode order.
- /// </summary>
- public const string AiredEpisodeOrder = "AiredEpisodeOrder";
-
- /// <summary>
- /// The album.
- /// </summary>
- public const string Album = "Album";
-
- /// <summary>
- /// The album artist.
- /// </summary>
- public const string AlbumArtist = "AlbumArtist";
-
- /// <summary>
- /// The artist.
- /// </summary>
- public const string Artist = "Artist";
-
- /// <summary>
- /// The date created.
- /// </summary>
- public const string DateCreated = "DateCreated";
-
- /// <summary>
- /// The official rating.
- /// </summary>
- public const string OfficialRating = "OfficialRating";
-
- /// <summary>
- /// The date played.
- /// </summary>
- public const string DatePlayed = "DatePlayed";
-
- /// <summary>
- /// The premiere date.
- /// </summary>
- public const string PremiereDate = "PremiereDate";
-
- /// <summary>
- /// The start date.
- /// </summary>
- public const string StartDate = "StartDate";
-
- /// <summary>
- /// The sort name.
- /// </summary>
- public const string SortName = "SortName";
-
- /// <summary>
- /// The name.
- /// </summary>
- public const string Name = "Name";
-
- /// <summary>
- /// The random.
- /// </summary>
- public const string Random = "Random";
-
- /// <summary>
- /// The runtime.
- /// </summary>
- public const string Runtime = "Runtime";
-
- /// <summary>
- /// The community rating.
- /// </summary>
- public const string CommunityRating = "CommunityRating";
-
- /// <summary>
- /// The production year.
- /// </summary>
- public const string ProductionYear = "ProductionYear";
-
- /// <summary>
- /// The play count.
- /// </summary>
- public const string PlayCount = "PlayCount";
-
- /// <summary>
- /// The critic rating.
- /// </summary>
- public const string CriticRating = "CriticRating";
-
- /// <summary>
- /// The IsFolder boolean.
- /// </summary>
- public const string IsFolder = "IsFolder";
-
- /// <summary>
- /// The IsUnplayed boolean.
- /// </summary>
- public const string IsUnplayed = "IsUnplayed";
-
- /// <summary>
- /// The IsPlayed boolean.
- /// </summary>
- public const string IsPlayed = "IsPlayed";
-
- /// <summary>
- /// The series sort.
- /// </summary>
- public const string SeriesSortName = "SeriesSortName";
-
- /// <summary>
- /// The video bitrate.
- /// </summary>
- public const string VideoBitRate = "VideoBitRate";
-
- /// <summary>
- /// The air time.
- /// </summary>
- public const string AirTime = "AirTime";
-
- /// <summary>
- /// The studio.
- /// </summary>
- public const string Studio = "Studio";
-
- /// <summary>
- /// The IsFavouriteOrLiked boolean.
- /// </summary>
- public const string IsFavoriteOrLiked = "IsFavoriteOrLiked";
-
- /// <summary>
- /// The last content added date.
- /// </summary>
- public const string DateLastContentAdded = "DateLastContentAdded";
-
- /// <summary>
- /// The series last played date.
- /// </summary>
- public const string SeriesDatePlayed = "SeriesDatePlayed";
-
- /// <summary>
- /// The parent index number.
- /// </summary>
- public const string ParentIndexNumber = "ParentIndexNumber";
-
- /// <summary>
- /// The index number.
- /// </summary>
- public const string IndexNumber = "IndexNumber";
-
- /// <summary>
- /// The similarity score.
- /// </summary>
- public const string SimilarityScore = "SimilarityScore";
-
- /// <summary>
- /// The search score.
- /// </summary>
- public const string SearchScore = "SearchScore";
- }
-}
diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs
index 3fa7f3d56..fd911dbed 100644
--- a/MediaBrowser.Model/Search/SearchHint.cs
+++ b/MediaBrowser.Model/Search/SearchHint.cs
@@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Search
{
Name = string.Empty;
MatchedTerm = string.Empty;
- MediaType = string.Empty;
+ MediaType = Jellyfin.Data.Enums.MediaType.Unknown;
Artists = Array.Empty<string>();
}
@@ -115,7 +115,7 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the type of the media.
/// </summary>
/// <value>The type of the media.</value>
- public string MediaType { get; set; }
+ public MediaType MediaType { get; set; }
/// <summary>
/// Gets or sets the start date.
diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs
index 1caed827f..b91fd8657 100644
--- a/MediaBrowser.Model/Search/SearchQuery.cs
+++ b/MediaBrowser.Model/Search/SearchQuery.cs
@@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Search
IncludePeople = true;
IncludeStudios = true;
- MediaTypes = Array.Empty<string>();
+ MediaTypes = Array.Empty<MediaType>();
IncludeItemTypes = Array.Empty<BaseItemKind>();
ExcludeItemTypes = Array.Empty<BaseItemKind>();
}
@@ -55,7 +55,7 @@ namespace MediaBrowser.Model.Search
public bool IncludeArtists { get; set; }
- public string[] MediaTypes { get; set; }
+ public MediaType[] MediaTypes { get; set; }
public BaseItemKind[] IncludeItemTypes { get; set; }
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
index d692906c6..7fefce9cd 100644
--- a/MediaBrowser.Model/Session/ClientCapabilities.cs
+++ b/MediaBrowser.Model/Session/ClientCapabilities.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dlna;
namespace MediaBrowser.Model.Session
@@ -11,12 +12,12 @@ namespace MediaBrowser.Model.Session
{
public ClientCapabilities()
{
- PlayableMediaTypes = Array.Empty<string>();
+ PlayableMediaTypes = Array.Empty<MediaType>();
SupportedCommands = Array.Empty<GeneralCommandType>();
SupportsPersistentIdentifier = true;
}
- public IReadOnlyList<string> PlayableMediaTypes { get; set; }
+ public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; }
public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; }
diff --git a/MediaBrowser.Model/System/CastReceiverApplication.cs b/MediaBrowser.Model/System/CastReceiverApplication.cs
new file mode 100644
index 000000000..6a49a5cac
--- /dev/null
+++ b/MediaBrowser.Model/System/CastReceiverApplication.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.System;
+
+/// <summary>
+/// The cast receiver application model.
+/// </summary>
+public class CastReceiverApplication
+{
+ /// <summary>
+ /// Gets or sets the cast receiver application id.
+ /// </summary>
+ public required string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cast receiver application name.
+ /// </summary>
+ public required string Name { get; set; }
+}
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index 502fc38ab..aa7c03ebd 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using System.Runtime.InteropServices;
using MediaBrowser.Model.Updates;
@@ -130,6 +131,11 @@ namespace MediaBrowser.Model.System
public string TranscodingTempPath { get; set; }
/// <summary>
+ /// Gets or sets the list of cast receiver applications.
+ /// </summary>
+ public IReadOnlyList<CastReceiverApplication> CastReceiverApplications { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance has update available.
/// </summary>
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 8354c60ef..219ed5d5f 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -3,6 +3,7 @@
using System;
using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule;
@@ -15,6 +16,7 @@ namespace MediaBrowser.Model.Users
{
IsHidden = true;
EnableCollectionManagement = false;
+ EnableSubtitleManagement = false;
EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty<string>();
@@ -84,6 +86,13 @@ namespace MediaBrowser.Model.Users
public bool EnableCollectionManagement { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether this instance can manage subtitles.
+ /// </summary>
+ /// <value><c>true</c> if this instance is allowed; otherwise, <c>false</c>.</value>
+ [DefaultValue(false)]
+ public bool EnableSubtitleManagement { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance is disabled.
/// </summary>
/// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>
@@ -166,8 +175,10 @@ namespace MediaBrowser.Model.Users
public int RemoteClientBitrateLimit { get; set; }
[XmlElement(ElementName = "AuthenticationProviderId")]
+ [Required(AllowEmptyStrings = false)]
public string AuthenticationProviderId { get; set; }
+ [Required(AllowEmptyStrings= false)]
public string PasswordResetProviderId { get; set; }
/// <summary>
diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
index 7f1ecd743..a10ff198b 100644
--- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
+++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
@@ -125,7 +125,7 @@ public class LrcLyricParser : ILyricParser
/// </summary>
/// <param name="metaData">The metadata from the LRC file.</param>
/// <returns>A lyricMetadata object with mapped property data.</returns>
- private static LyricMetadata MapMetadataValues(IDictionary<string, string> metaData)
+ private static LyricMetadata MapMetadataValues(Dictionary<string, string> metaData)
{
LyricMetadata lyricMetadata = new();
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index dab36625e..1a5dbd7a5 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -175,7 +175,7 @@ namespace MediaBrowser.Providers.Manager
IDynamicImageProvider provider,
ImageRefreshOptions refreshOptions,
TypeOptions savedOptions,
- ICollection<ImageType> downloadedImages,
+ List<ImageType> downloadedImages,
RefreshResult result,
CancellationToken cancellationToken)
{
@@ -263,7 +263,7 @@ namespace MediaBrowser.Providers.Manager
ImageRefreshOptions refreshOptions,
TypeOptions savedOptions,
int backdropLimit,
- ICollection<ImageType> downloadedImages,
+ List<ImageType> downloadedImages,
RefreshResult result,
CancellationToken cancellationToken)
{
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 75291b317..06445c90d 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -8,11 +8,11 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index d0bb34d52..4ba884418 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1096,13 +1096,13 @@ namespace MediaBrowser.Providers.Manager
return;
}
- if (!_disposeCancellationTokenSource.IsCancellationRequested)
- {
- _disposeCancellationTokenSource.Cancel();
- }
-
if (disposing)
{
+ if (!_disposeCancellationTokenSource.IsCancellationRequested)
+ {
+ _disposeCancellationTokenSource.Cancel();
+ }
+
_disposeCancellationTokenSource.Dispose();
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 6a40833d7..7a50c6cf4 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.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>
@@ -27,14 +27,18 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 44f998742..d81704227 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaSourceManager = mediaSourceManager;
}
- [GeneratedRegex("I:\\s+(.*?)\\s+LUFS")]
+ [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
private static partial Regex LUFSRegex();
/// <summary>
@@ -107,7 +107,6 @@ namespace MediaBrowser.Providers.MediaInfo
if (libraryOptions.EnableLUFSScan)
{
- string output;
using (var process = new Process()
{
StartInfo = new ProcessStartInfo
@@ -131,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
using var reader = process.StandardError;
- output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+ var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
MatchCollection split = LUFSRegex().Matches(output);
diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index b4b1895f5..d1c0ddb37 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -82,8 +82,8 @@ namespace MediaBrowser.Providers.MediaInfo
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
- var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("front", StringComparison.OrdinalIgnoreCase) != -1) ??
- imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("cover", StringComparison.OrdinalIgnoreCase) != -1) ??
+ var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase)) ??
+ imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase)) ??
imageStreams.FirstOrDefault();
var imageStreamIndex = imageStream?.Index;
diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
index 909cbb9b9..f846aa5de 100644
--- a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
@@ -183,7 +183,7 @@ namespace MediaBrowser.Providers.MediaInfo
files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true));
}
- if (!files.Any())
+ if (files.Count == 0)
{
return Array.Empty<ExternalPathParserResult>();
}
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index f21939d2a..6eb75891a 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -97,7 +97,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
var query = new InternalItemsQuery
{
- MediaTypes = new string[] { MediaType.Video },
+ MediaTypes = new[] { MediaType.Video },
IsVirtualItem = false,
IncludeItemTypes = types,
DtoOptions = new DtoOptions(true),
diff --git a/MediaBrowser.Providers/Movies/ImdbExternalId.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
index d00f37db5..a8d74aa0b 100644
--- a/MediaBrowser.Providers/Movies/ImdbExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.imdb.com/title/{0}";
+ public string UrlFormatString => "https://www.imdb.com/title/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
index 1bb5e1ea8..8151ab471 100644
--- a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.imdb.com/name/{0}";
+ public string UrlFormatString => "https://www.imdb.com/name/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Person;
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 0ddb2ad67..e4f34776b 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -148,7 +148,7 @@ namespace MediaBrowser.Providers.Music
.ToArray();
var id = item.GetProviderId(provider);
- if (ids.Any())
+ if (ids.Length != 0)
{
var firstId = ids[0];
if (!string.IsNullOrEmpty(firstId)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
index 3a400575b..138cfef19 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+ public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
index 7f73afc53..8a516e1ce 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
@@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
return Enumerable.Empty<RemoteImageInfo>();
}
- private IEnumerable<RemoteImageInfo> GetImages(AudioDbAlbumProvider.Album item)
+ private List<RemoteImageInfo> GetImages(AudioDbAlbumProvider.Album item)
{
var list = new List<RemoteImageInfo>();
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
index b9e57eb26..8aceb48c0 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+ public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
index 2232dfa0d..4e7757cd2 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
@@ -78,7 +78,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
return Enumerable.Empty<RemoteImageInfo>();
}
- private IEnumerable<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item)
+ private List<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item)
{
var list = new List<RemoteImageInfo>();
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
index f8f6253ff..014481da2 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+ public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
index fd598c918..787539104 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+ public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
index f7850781e..825fe32fa 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzAlbumArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
index a9d4472e7..b7d53984c 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzAlbumExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
index b89e67270..b3f001618 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
index fdaa5574f..a0a922293 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzOtherArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
index 0baab9955..47b6d6963 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzReleaseGroupExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
index 5c974c411..cb4345660 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzTrackId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
index 0e768bb83..d453a4ff4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
/// <inheritdoc />
- public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
index 38d2c5c69..6d6032e8f 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
/// <inheritdoc />
- public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
index 027399aec..d26a70028 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index f18575aa9..489f5e2a1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
+using TMDbLib.Objects.TvShows;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
@@ -102,9 +104,61 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return metadataResult;
}
- var episodeResult = await _tmdbClientManager
- .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
- .ConfigureAwait(false);
+ TvEpisode? episodeResult = null;
+ if (info.IndexNumberEnd.HasValue)
+ {
+ var startindex = episodeNumber;
+ var endindex = info.IndexNumberEnd;
+ List<TvEpisode>? result = null;
+ for (int? episode = startindex; episode <= endindex; episode++)
+ {
+ var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
+ if (episodeInfo is not null)
+ {
+ (result ??= new List<TvEpisode>()).Add(episodeInfo);
+ }
+ }
+
+ if (result is not null)
+ {
+ // Forces a deep copy of the first TvEpisode, so we don't modify the original because it's cached
+ episodeResult = new TvEpisode()
+ {
+ Name = result[0].Name,
+ Overview = result[0].Overview,
+ AirDate = result[0].AirDate,
+ VoteAverage = result[0].VoteAverage,
+ ExternalIds = result[0].ExternalIds,
+ Videos = result[0].Videos,
+ Credits = result[0].Credits
+ };
+
+ if (result.Count > 1)
+ {
+ var name = new StringBuilder(episodeResult.Name);
+ var overview = new StringBuilder(episodeResult.Overview);
+
+ for (int i = 1; i < result.Count; i++)
+ {
+ name.Append(" / ").Append(result[i].Name);
+ overview.Append(" / ").Append(result[i].Overview);
+ }
+
+ episodeResult.Name = name.ToString();
+ episodeResult.Overview = overview.ToString();
+ }
+ }
+ else
+ {
+ return metadataResult;
+ }
+ }
+ else
+ {
+ episodeResult = await _tmdbClientManager
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
+ }
if (episodeResult is null)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
index df04cb2e7..5f2d7909a 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
/// <inheritdoc />
- public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 500ebaf71..72e59c9ac 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index e01c0f483..01c07d633 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -121,7 +121,7 @@ namespace MediaBrowser.Providers.TV
var seasonNumber = virtualSeason.IndexNumber;
// If there's a physical season with the same number or no episodes in the season, delete it
if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
- || !virtualSeason.GetEpisodes().Any())
+ || virtualSeason.GetEpisodes().Count == 0)
{
Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name);
diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
index 087e4036a..3cb18e424 100644
--- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.TV
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string? UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
+ public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Series;
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
new file mode 100644
index 000000000..90c2ff8dd
--- /dev/null
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Trickplay;
+
+/// <summary>
+/// Class TrickplayImagesTask.
+/// </summary>
+public class TrickplayImagesTask : IScheduledTask
+{
+ private const int QueryPageLimit = 100;
+
+ private readonly ILogger<TrickplayImagesTask> _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly ITrickplayManager _trickplayManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayImagesTask"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="localization">The localization manager.</param>
+ /// <param name="trickplayManager">The trickplay manager.</param>
+ public TrickplayImagesTask(
+ ILogger<TrickplayImagesTask> logger,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ ITrickplayManager trickplayManager)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _localization = localization;
+ _trickplayManager = trickplayManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription");
+
+ /// <inheritdoc />
+ public string Key => "RefreshTrickplayImages";
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[]
+ {
+ new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerDaily,
+ TimeOfDayTicks = TimeSpan.FromHours(3).Ticks
+ }
+ };
+ }
+
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var query = new InternalItemsQuery
+ {
+ MediaTypes = new[] { MediaType.Video },
+ SourceTypes = new[] { SourceType.Library },
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true,
+ Limit = QueryPageLimit
+ };
+
+ var numberOfVideos = _libraryManager.GetCount(query);
+
+ var startIndex = 0;
+ var numComplete = 0;
+
+ while (startIndex < numberOfVideos)
+ {
+ query.StartIndex = startIndex;
+ var videos = _libraryManager.GetItemList(query).OfType<Video>();
+
+ foreach (var video in videos)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ await _trickplayManager.RefreshTrickplayDataAsync(video, false, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating trickplay files for {ItemName}", video.Name);
+ }
+
+ numComplete++;
+ progress.Report(100d * numComplete / numberOfVideos);
+ }
+
+ startIndex += QueryPageLimit;
+ }
+
+ progress.Report(100);
+ }
+}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
new file mode 100644
index 000000000..f6dcde4f6
--- /dev/null
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -0,0 +1,121 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Providers.Trickplay;
+
+/// <summary>
+/// Class TrickplayProvider. Provides images and metadata for trickplay
+/// scrubbing previews.
+/// </summary>
+public class TrickplayProvider : ICustomMetadataProvider<Episode>,
+ ICustomMetadataProvider<MusicVideo>,
+ ICustomMetadataProvider<Movie>,
+ ICustomMetadataProvider<Trailer>,
+ ICustomMetadataProvider<Video>,
+ IHasItemChangeMonitor,
+ IHasOrder,
+ IForcedProvider
+{
+ private readonly IServerConfigurationManager _config;
+ private readonly ITrickplayManager _trickplayManager;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayProvider"/> class.
+ /// </summary>
+ /// <param name="config">The configuration manager.</param>
+ /// <param name="trickplayManager">The trickplay manager.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ public TrickplayProvider(
+ IServerConfigurationManager config,
+ ITrickplayManager trickplayManager,
+ ILibraryManager libraryManager)
+ {
+ _config = config;
+ _trickplayManager = trickplayManager;
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Trickplay Provider";
+
+ /// <inheritdoc />
+ public int Order => 100;
+
+ /// <inheritdoc />
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ if (item.IsFileProtocol)
+ {
+ var file = directoryService.GetFile(item.Path);
+ if (file is not null && item.DateModified != file.LastWriteTimeUtc)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ private async Task<ItemUpdateType> FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
+ bool replace = options.ReplaceAllImages;
+
+ if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
+ {
+ return ItemUpdateType.None;
+ }
+
+ if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking)
+ {
+ await _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ _ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
+ }
+
+ // The core doesn't need to trigger any save operations over this
+ return ItemUpdateType.None;
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 807234915..c20073eea 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -15,13 +15,17 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 47a127950..70e5b66c1 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -160,7 +160,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
// Find last closing Tag
// Need to do this in two steps to account for random > characters after the closing xml
- var index = xml.LastIndexOf(@"</", StringComparison.Ordinal);
+ var index = xml.LastIndexOf("</", StringComparison.Ordinal);
// If closing tag exists, move to end of Tag
if (index != -1)
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
index af581fc5d..9b4e1731d 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
@@ -13,7 +13,7 @@ namespace MediaBrowser.XbmcMetadata.Providers
public abstract class BaseNfoProvider<T> : ILocalMetadataProvider<T>, IHasItemChangeMonitor
where T : BaseItem, new()
{
- private IFileSystem _fileSystem;
+ private readonly IFileSystem _fileSystem;
protected BaseNfoProvider(IFileSystem fileSystem)
{
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index bf66a3145..1399ac307 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -314,11 +314,11 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var codec = stream.Codec;
- if ((stream.CodecTag ?? string.Empty).IndexOf("xvid", StringComparison.OrdinalIgnoreCase) != -1)
+ if ((stream.CodecTag ?? string.Empty).Contains("xvid", StringComparison.OrdinalIgnoreCase))
{
codec = "xvid";
}
- else if ((stream.CodecTag ?? string.Empty).IndexOf("divx", StringComparison.OrdinalIgnoreCase) != -1)
+ else if ((stream.CodecTag ?? string.Empty).Contains("divx", StringComparison.OrdinalIgnoreCase))
{
codec = "divx";
}
diff --git a/README.md b/README.md
index 2362741b4..15dd0ae67 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,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 7.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system.
+Before the project can be built, you must first install the [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet) 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 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download).
@@ -137,7 +137,7 @@ A second option is to build the project and then run the resulting executable fi
```bash
dotnet build # Build the project
-cd Jellyfin.Server/bin/Debug/net7.0 # Change into the build output directory
+cd Jellyfin.Server/bin/Debug/net8.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/DeviceAvailableEventArgs.cs b/RSSDP/DeviceAvailableEventArgs.cs
deleted file mode 100644
index f933f258b..000000000
--- a/RSSDP/DeviceAvailableEventArgs.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.Net;
-
-namespace Rssdp
-{
- /// <summary>
- /// Event arguments for the <see cref="Infrastructure.SsdpDeviceLocator.DeviceAvailable"/> event.
- /// </summary>
- public sealed class DeviceAvailableEventArgs : EventArgs
- {
- public IPAddress RemoteIPAddress { get; set; }
-
- private readonly DiscoveredSsdpDevice _DiscoveredDevice;
-
- private readonly bool _IsNewlyDiscovered;
-
- /// <summary>
- /// Full constructor.
- /// </summary>
- /// <param name="discoveredDevice">A <see cref="DiscoveredSsdpDevice"/> instance representing the available device.</param>
- /// <param name="isNewlyDiscovered">A boolean value indicating whether or not this device came from the cache. See <see cref="IsNewlyDiscovered"/> for more detail.</param>
- /// <exception cref="ArgumentNullException">Thrown if the <paramref name="discoveredDevice"/> parameter is null.</exception>
- public DeviceAvailableEventArgs(DiscoveredSsdpDevice discoveredDevice, bool isNewlyDiscovered)
- {
- if (discoveredDevice == null)
- {
- throw new ArgumentNullException(nameof(discoveredDevice));
- }
-
- _DiscoveredDevice = discoveredDevice;
- _IsNewlyDiscovered = isNewlyDiscovered;
- }
-
- /// <summary>
- /// Returns true if the device was discovered due to an alive notification, or a search and was not already in the cache. Returns false if the item came from the cache but matched the current search request.
- /// </summary>
- public bool IsNewlyDiscovered
- {
- get { return _IsNewlyDiscovered; }
- }
-
- /// <summary>
- /// A reference to a <see cref="DiscoveredSsdpDevice"/> instance containing the discovered details and allowing access to the full device description.
- /// </summary>
- public DiscoveredSsdpDevice DiscoveredDevice
- {
- get { return _DiscoveredDevice; }
- }
- }
-}
diff --git a/RSSDP/DeviceEventArgs.cs b/RSSDP/DeviceEventArgs.cs
deleted file mode 100644
index 2455ccbfa..000000000
--- a/RSSDP/DeviceEventArgs.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System;
-
-namespace Rssdp
-{
- /// <summary>
- /// Event arguments for the <see cref="SsdpDevice.DeviceAdded"/> and <see cref="SsdpDevice.DeviceRemoved"/> events.
- /// </summary>
- public sealed class DeviceEventArgs : EventArgs
- {
- private readonly SsdpDevice _Device;
-
- /// <summary>
- /// Constructs a new instance for the specified <see cref="SsdpDevice"/>.
- /// </summary>
- /// <param name="device">The <see cref="SsdpDevice"/> associated with the event this argument class is being used for.</param>
- /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
- public DeviceEventArgs(SsdpDevice device)
- {
- if (device == null)
- {
- throw new ArgumentNullException(nameof(device));
- }
-
- _Device = device;
- }
-
- /// <summary>
- /// Returns the <see cref="SsdpDevice"/> instance the event being raised for.
- /// </summary>
- public SsdpDevice Device
- {
- get { return _Device; }
- }
- }
-}
diff --git a/RSSDP/DeviceUnavailableEventArgs.cs b/RSSDP/DeviceUnavailableEventArgs.cs
deleted file mode 100644
index ca2515202..000000000
--- a/RSSDP/DeviceUnavailableEventArgs.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System;
-
-namespace Rssdp
-{
- /// <summary>
- /// Event arguments for the <see cref="Infrastructure.SsdpDeviceLocator.DeviceUnavailable"/> event.
- /// </summary>
- public sealed class DeviceUnavailableEventArgs : EventArgs
- {
- private readonly DiscoveredSsdpDevice _DiscoveredDevice;
-
- private readonly bool _Expired;
-
- /// <summary>
- /// Full constructor.
- /// </summary>
- /// <param name="discoveredDevice">A <see cref="DiscoveredSsdpDevice"/> instance representing the device that has become unavailable.</param>
- /// <param name="expired">A boolean value indicating whether this device is unavailable because it expired, or because it explicitly sent a byebye notification.. See <see cref="Expired"/> for more detail.</param>
- /// <exception cref="ArgumentNullException">Thrown if the <paramref name="discoveredDevice"/> parameter is null.</exception>
- public DeviceUnavailableEventArgs(DiscoveredSsdpDevice discoveredDevice, bool expired)
- {
- if (discoveredDevice == null)
- {
- throw new ArgumentNullException(nameof(discoveredDevice));
- }
-
- _DiscoveredDevice = discoveredDevice;
- _Expired = expired;
- }
-
- /// <summary>
- /// Returns true if the device is considered unavailable because it's cached information expired before a new alive notification or search result was received. Returns false if the device is unavailable because it sent an explicit notification of it's unavailability.
- /// </summary>
- public bool Expired
- {
- get { return _Expired; }
- }
-
- /// <summary>
- /// A reference to a <see cref="DiscoveredSsdpDevice"/> instance containing the discovery details of the removed device.
- /// </summary>
- public DiscoveredSsdpDevice DiscoveredDevice
- {
- get { return _DiscoveredDevice; }
- }
- }
-}
diff --git a/RSSDP/DiscoveredSsdpDevice.cs b/RSSDP/DiscoveredSsdpDevice.cs
deleted file mode 100644
index 322bd55e5..000000000
--- a/RSSDP/DiscoveredSsdpDevice.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using System;
-using System.Net.Http.Headers;
-
-namespace Rssdp
-{
- /// <summary>
- /// Represents a discovered device, containing basic information about the device and the location of it's full device description document. Also provides convenience methods for retrieving the device description document.
- /// </summary>
- /// <seealso cref="SsdpDevice"/>
- /// <seealso cref="Infrastructure.ISsdpDeviceLocator"/>
- public sealed class DiscoveredSsdpDevice
- {
- private DateTimeOffset _AsAt;
-
- /// <summary>
- /// Sets or returns the type of notification, being either a uuid, device type, service type or upnp:rootdevice.
- /// </summary>
- public string NotificationType { get; set; }
-
- /// <summary>
- /// Sets or returns the universal service name (USN) of the device.
- /// </summary>
- public string Usn { get; set; }
-
- /// <summary>
- /// Sets or returns a URL pointing to the device description document for this device.
- /// </summary>
- public Uri DescriptionLocation { get; set; }
-
- /// <summary>
- /// Sets or returns the length of time this information is valid for (from the <see cref="AsAt"/> time).
- /// </summary>
- public TimeSpan CacheLifetime { get; set; }
-
- /// <summary>
- /// Sets or returns the date and time this information was received.
- /// </summary>
- public DateTimeOffset AsAt
- {
- get { return _AsAt; }
-
- set
- {
- if (_AsAt != value)
- {
- _AsAt = value;
- }
- }
- }
-
- /// <summary>
- /// Returns the headers from the SSDP device response message.
- /// </summary>
- public HttpHeaders ResponseHeaders { get; set; }
-
- /// <summary>
- /// Returns true if this device information has expired, based on the current date/time, and the <see cref="CacheLifetime"/> &amp; <see cref="AsAt"/> properties.
- /// </summary>
- /// <returns></returns>
- public bool IsExpired()
- {
- return this.CacheLifetime == TimeSpan.Zero || this.AsAt.Add(this.CacheLifetime) <= DateTimeOffset.Now;
- }
-
- /// <summary>
- /// Returns the device's <see cref="Usn"/> value.
- /// </summary>
- /// <returns>A string containing the device's universal service name.</returns>
- public override string ToString()
- {
- return this.Usn;
- }
- }
-}
diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs
deleted file mode 100644
index 5d7da4124..000000000
--- a/RSSDP/DisposableManagedObjectBase.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Text;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// 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
- {
- /// <summary>
- /// Override this method and dispose any objects you own the lifetime of if disposing is true;
- /// </summary>
- /// <param name="disposing">True if managed objects should be disposed, if false, only unmanaged resources should be released.</param>
- protected abstract void Dispose(bool disposing);
-
- /// <summary>
- /// Throws and <see cref="ObjectDisposedException"/> if the <see cref="IsDisposed"/> property is true.
- /// </summary>
- /// <seealso cref="IsDisposed"/>
- /// <exception cref="ObjectDisposedException">Thrown if the <see cref="IsDisposed"/> property is true.</exception>
- /// <seealso cref="Dispose()"/>
- protected virtual void ThrowIfDisposed()
- {
- if (this.IsDisposed)
- {
- throw new ObjectDisposedException(this.GetType().FullName);
- }
- }
-
- /// <summary>
- /// Sets or returns a boolean indicating whether or not this instance has been disposed.
- /// </summary>
- /// <seealso cref="Dispose()"/>
- public bool IsDisposed
- {
- get;
- private set;
- }
-
- public string BuildMessage(string header, Dictionary<string, string> values)
- {
- var builder = new StringBuilder();
-
- const string ArgFormat = "{0}: {1}\r\n";
-
- builder.AppendFormat(CultureInfo.InvariantCulture, "{0}\r\n", header);
-
- foreach (var pair in values)
- {
- builder.AppendFormat(CultureInfo.InvariantCulture, ArgFormat, pair.Key, pair.Value);
- }
-
- builder.Append("\r\n");
-
- return builder.ToString();
- }
-
- /// <summary>
- /// 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 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 interfere with the dispose process.")]
- public void Dispose()
- {
- IsDisposed = true;
-
- Dispose(true);
- }
- }
-}
diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs
deleted file mode 100644
index 1949a9df3..000000000
--- a/RSSDP/HttpParserBase.cs
+++ /dev/null
@@ -1,228 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// A base class for the <see cref="HttpResponseParser"/> and <see cref="HttpRequestParser"/> classes. Not intended for direct use.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- public abstract class HttpParserBase<T> where T : new()
- {
- private readonly string[] LineTerminators = new string[] { "\r\n", "\n" };
- private readonly char[] SeparatorCharacters = new char[] { ',', ';' };
-
- /// <summary>
- /// Parses the <paramref name="data"/> provided into either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.
- /// </summary>
- /// <param name="data">A string containing the HTTP message to parse.</param>
- /// <returns>Either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object containing the parsed data.</returns>
- public abstract T Parse(string data);
-
- /// <summary>
- /// Parses a string containing either an HTTP request or response into a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.
- /// </summary>
- /// <param name="message">A <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object representing the parsed message.</param>
- /// <param name="headers">A reference to the <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the <paramref name="message"/> object.</param>
- /// <param name="data">A string containing the data to be parsed.</param>
- /// <returns>An <see cref="HttpContent"/> object containing the content of the parsed message.</returns>
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Honestly, it's fine. MemoryStream doesn't mind.")]
- protected virtual void Parse(T message, System.Net.Http.Headers.HttpHeaders headers, string data)
- {
- if (data == null)
- {
- throw new ArgumentNullException(nameof(data));
- }
-
- if (data.Length == 0)
- {
- throw new ArgumentException("data cannot be an empty string.", nameof(data));
- }
-
- if (!LineTerminators.Any(data.Contains))
- {
- throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", nameof(data));
- }
-
- using (var retVal = new ByteArrayContent(Array.Empty<byte>()))
- {
- var lines = data.Split(LineTerminators, StringSplitOptions.None);
-
- // First line is the 'request' line containing http protocol details like method, uri, http version etc.
- ParseStatusLine(lines[0], message);
-
- ParseHeaders(headers, retVal.Headers, lines);
- }
- }
-
- /// <summary>
- /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>.
- /// </summary>
- /// <param name="data">The first line of the HTTP message to be parsed.</param>
- /// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param>
- protected abstract void ParseStatusLine(string data, T message);
-
- /// <summary>
- /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false).
- /// </summary>
- /// <param name="headerName">A string containing the name of the header to return the type of.</param>
- protected abstract bool IsContentHeader(string headerName);
-
- /// <summary>
- /// Parses the HTTP version text from an HTTP request or response status line and returns a <see cref="Version"/> object representing the parsed values.
- /// </summary>
- /// <param name="versionData">A string containing the HTTP version, from the message status line.</param>
- /// <returns>A <see cref="Version"/> object containing the parsed version data.</returns>
- protected Version ParseHttpVersion(string versionData)
- {
- if (versionData == null)
- {
- throw new ArgumentNullException(nameof(versionData));
- }
-
- var versionSeparatorIndex = versionData.IndexOf('/', StringComparison.Ordinal);
- if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length)
- {
- throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", nameof(versionData));
- }
-
- return Version.Parse(versionData.Substring(versionSeparatorIndex + 1));
- }
-
- /// <summary>
- /// Parses a line from an HTTP request or response message containing a header name and value pair.
- /// </summary>
- /// <param name="line">A string containing the data to be parsed.</param>
- /// <param name="headers">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection to which the parsed header will be added.</param>
- /// <param name="contentHeaders">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the message content, to which the parsed header will be added.</param>
- private void ParseHeader(string line, System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders)
- {
- // Header format is
- // name: value
- var headerKeySeparatorIndex = line.IndexOf(':', StringComparison.Ordinal);
- var headerName = line.Substring(0, headerKeySeparatorIndex).Trim();
- var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim();
-
- // 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.
-
- var values = ParseValues(headerValue);
- var headersToAddTo = IsContentHeader(headerName) ? contentHeaders : headers;
-
- if (values.Count > 1)
- {
- headersToAddTo.TryAddWithoutValidation(headerName, values);
- }
- else
- {
- headersToAddTo.TryAddWithoutValidation(headerName, values[0]);
- }
- }
-
- private int ParseHeaders(System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders, string[] lines)
- {
- // Blank line separates headers from content, so read headers until we find blank line.
- int lineIndex = 1;
- string line = null, nextLine = null;
- while (lineIndex + 1 < lines.Length && !String.IsNullOrEmpty((line = lines[lineIndex++])))
- {
- // If the following line starts with space or tab (or any whitespace), it is really part of this header but split for human readability.
- // Combine these lines into a single comma separated style header for easier parsing.
- while (lineIndex < lines.Length && !String.IsNullOrEmpty((nextLine = lines[lineIndex])))
- {
- if (nextLine.Length > 0 && Char.IsWhiteSpace(nextLine[0]))
- {
- line += "," + nextLine.TrimStart();
- lineIndex++;
- }
- else
- {
- break;
- }
- }
-
- ParseHeader(line, headers, contentHeaders);
- }
-
- return lineIndex;
- }
-
- private List<string> ParseValues(string headerValue)
- {
- // This really should be better and match the HTTP 1.1 spec,
- // but this should actually be good enough for SSDP implementations
- // I think.
- var values = new List<string>();
-
- if (headerValue == "\"\"")
- {
- values.Add(string.Empty);
- return values;
- }
-
- var indexOfSeparator = headerValue.IndexOfAny(SeparatorCharacters);
- if (indexOfSeparator <= 0)
- {
- values.Add(headerValue);
- }
- else
- {
- var segments = headerValue.Split(SeparatorCharacters);
- if (headerValue.Contains('"', StringComparison.Ordinal))
- {
- for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++)
- {
- var segment = segments[segmentIndex];
- if (segment.Trim().StartsWith("\"", StringComparison.OrdinalIgnoreCase))
- {
- segment = CombineQuotedSegments(segments, ref segmentIndex, segment);
- }
-
- values.Add(segment);
- }
- }
- else
- {
- values.AddRange(segments);
- }
- }
-
- return values;
- }
-
- private string CombineQuotedSegments(string[] segments, ref int segmentIndex, string segment)
- {
- var trimmedSegment = segment.Trim();
- for (int index = segmentIndex; index < segments.Length; index++)
- {
- if (trimmedSegment == "\"\"" ||
- (
- trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase)
- && !trimmedSegment.EndsWith("\"\"", StringComparison.OrdinalIgnoreCase)
- && !trimmedSegment.EndsWith("\\\"", StringComparison.OrdinalIgnoreCase))
- )
- {
- segmentIndex = index;
- return trimmedSegment.Substring(1, trimmedSegment.Length - 2);
- }
-
- if (index + 1 < segments.Length)
- {
- trimmedSegment += "," + segments[index + 1].TrimEnd();
- }
- }
-
- segmentIndex = segments.Length;
- if (trimmedSegment.StartsWith("\"", StringComparison.OrdinalIgnoreCase) && trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase))
- {
- return trimmedSegment.Substring(1, trimmedSegment.Length - 2);
- }
-
- return trimmedSegment;
- }
- }
-}
diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs
deleted file mode 100644
index a1b4627a9..000000000
--- a/RSSDP/HttpRequestParser.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System;
-using System.Net.Http;
-using Jellyfin.Extensions;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Parses a string into a <see cref="HttpRequestMessage"/> or throws an exception.
- /// </summary>
- public sealed class HttpRequestParser : HttpParserBase<HttpRequestMessage>
- {
- private readonly string[] ContentHeaderNames = new string[]
- {
- "Allow", "Content-Disposition", "Content-Encoding", "Content-Language", "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", "Expires", "Last-Modified"
- };
-
- /// <summary>
- /// Parses the specified data into a <see cref="HttpRequestMessage"/> instance.
- /// </summary>
- /// <param name="data">A string containing the data to parse.</param>
- /// <returns>A <see cref="HttpRequestMessage"/> instance containing the parsed data.</returns>
- public override HttpRequestMessage Parse(string data)
- {
- HttpRequestMessage retVal = null;
-
- try
- {
- retVal = new HttpRequestMessage();
-
- Parse(retVal, retVal.Headers, data);
-
- return retVal;
- }
- finally
- {
- if (retVal != null)
- {
- retVal.Dispose();
- }
- }
- }
-
- /// <summary>
- /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>.
- /// </summary>
- /// <param name="data">The first line of the HTTP message to be parsed.</param>
- /// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param>
- protected override void ParseStatusLine(string data, HttpRequestMessage message)
- {
- if (data == null)
- {
- throw new ArgumentNullException(nameof(data));
- }
-
- if (message == null)
- {
- throw new ArgumentNullException(nameof(message));
- }
-
- var parts = data.Split(' ');
- if (parts.Length < 2)
- {
- throw new ArgumentException("Status line is invalid. Insufficient status parts.", nameof(data));
- }
-
- message.Method = new HttpMethod(parts[0].Trim());
- if (Uri.TryCreate(parts[1].Trim(), UriKind.RelativeOrAbsolute, out var requestUri))
- {
- message.RequestUri = requestUri;
- }
- else
- {
- System.Diagnostics.Debug.WriteLine(parts[1]);
- }
-
- if (parts.Length >= 3)
- {
- message.Version = ParseHttpVersion(parts[2].Trim());
- }
- }
-
- /// <summary>
- /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false).
- /// </summary>
- /// <param name="headerName">A string containing the name of the header to return the type of.</param>
- protected override bool IsContentHeader(string headerName)
- {
- return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase);
- }
- }
-}
diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs
deleted file mode 100644
index 71b7a7b99..000000000
--- a/RSSDP/HttpResponseParser.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using System;
-using System.Net;
-using System.Net.Http;
-using Jellyfin.Extensions;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Parses a string into a <see cref="HttpResponseMessage"/> or throws an exception.
- /// </summary>
- public sealed class HttpResponseParser : HttpParserBase<HttpResponseMessage>
- {
- private readonly string[] ContentHeaderNames = new string[]
- {
- "Allow", "Content-Disposition", "Content-Encoding", "Content-Language", "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", "Expires", "Last-Modified"
- };
-
- /// <summary>
- /// Parses the specified data into a <see cref="HttpResponseMessage"/> instance.
- /// </summary>
- /// <param name="data">A string containing the data to parse.</param>
- /// <returns>A <see cref="HttpResponseMessage"/> instance containing the parsed data.</returns>
- public override HttpResponseMessage Parse(string data)
- {
- HttpResponseMessage retVal = null;
- try
- {
- retVal = new HttpResponseMessage();
-
- Parse(retVal, retVal.Headers, data);
-
- return retVal;
- }
- catch
- {
- if (retVal != null)
- {
- retVal.Dispose();
- }
-
- throw;
- }
- }
-
- /// <summary>
- /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false).
- /// </summary>
- /// <param name="headerName">A string containing the name of the header to return the type of.</param>
- /// <returns>A boolean, true if th specified header relates to HTTP content, otherwise false.</returns>
- protected override bool IsContentHeader(string headerName)
- {
- return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase);
- }
-
- /// <summary>
- /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>.
- /// </summary>
- /// <param name="data">The first line of the HTTP message to be parsed.</param>
- /// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param>
- protected override void ParseStatusLine(string data, HttpResponseMessage message)
- {
- if (data == null)
- {
- throw new ArgumentNullException(nameof(data));
- }
-
- if (message == null)
- {
- throw new ArgumentNullException(nameof(message));
- }
-
- var parts = data.Split(' ');
- if (parts.Length < 2)
- {
- throw new ArgumentException("data status line is invalid. Insufficient status parts.", nameof(data));
- }
-
- message.Version = ParseHttpVersion(parts[0].Trim());
-
- if (!Int32.TryParse(parts[1].Trim(), out var statusCode))
- {
- throw new ArgumentException("data status line is invalid. Status code is not a valid integer.", nameof(data));
- }
-
- message.StatusCode = (HttpStatusCode)statusCode;
-
- if (parts.Length >= 3)
- {
- message.ReasonPhrase = parts[2].Trim();
- }
- }
- }
-}
diff --git a/RSSDP/IEnumerableExtensions.cs b/RSSDP/IEnumerableExtensions.cs
deleted file mode 100644
index 1f0daad3e..000000000
--- a/RSSDP/IEnumerableExtensions.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Rssdp.Infrastructure
-{
- internal static class IEnumerableExtensions
- {
- public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
- {
- if (source == null)
- {
- throw new ArgumentNullException(nameof(source));
- }
-
- if (selector == null)
- {
- throw new ArgumentNullException(nameof(selector));
- }
-
- return !source.Any() ? source :
- source.Concat(
- source
- .SelectMany(i => selector(i).EmptyIfNull())
- .SelectManyRecursive(selector)
- );
- }
-
- public static IEnumerable<T> EmptyIfNull<T>(this IEnumerable<T> source)
- {
- return source ?? Enumerable.Empty<T>();
- }
- }
-}
diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs
deleted file mode 100644
index 95b0a1c70..000000000
--- a/RSSDP/ISsdpCommunicationsServer.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Interface for a component that manages network communication (sending and receiving HTTPU messages) for the SSDP protocol.
- /// </summary>
- public interface ISsdpCommunicationsServer : IDisposable
- {
- /// <summary>
- /// Raised when a HTTPU request message is received by a socket (unicast or multicast).
- /// </summary>
- event EventHandler<RequestReceivedEventArgs> RequestReceived;
-
- /// <summary>
- /// Raised when an HTTPU response message is received by a socket (unicast or multicast).
- /// </summary>
- event EventHandler<ResponseReceivedEventArgs> ResponseReceived;
-
- /// <summary>
- /// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications.
- /// </summary>
- void BeginListeningForMulticast();
-
- /// <summary>
- /// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications.
- /// </summary>
- void StopListeningForMulticast();
-
- /// <summary>
- /// Sends a message to a particular address (uni or multicast) and port.
- /// </summary>
- Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIPAddress, CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends a message to the SSDP multicast address and port.
- /// </summary>
- Task SendMulticastMessage(string message, IPAddress fromLocalIPAddress, CancellationToken cancellationToken);
- Task SendMulticastMessage(string message, int sendCount, IPAddress fromLocalIPAddress, CancellationToken cancellationToken);
-
- /// <summary>
- /// Gets or sets a boolean value indicating whether or not this instance is shared amongst multiple <see cref="SsdpDeviceLocator"/> and/or <see cref="ISsdpDevicePublisher"/> instances.
- /// </summary>
- /// <remarks>
- /// <para>If true, disposing an instance of a <see cref="SsdpDeviceLocator"/>or a <see cref="ISsdpDevicePublisher"/> will not dispose this comms server instance. The calling code is responsible for managing the lifetime of the server.</para>
- /// </remarks>
- bool IsShared { get; set; }
- }
-}
diff --git a/RSSDP/ISsdpDeviceLocator.cs b/RSSDP/ISsdpDeviceLocator.cs
deleted file mode 100644
index 4df166cd2..000000000
--- a/RSSDP/ISsdpDeviceLocator.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-using System;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Interface for components that discover the existence of SSDP devices.
- /// </summary>
- /// <remarks>
- /// <para>Discovering devices includes explicit search requests as well as listening for broadcast status notifications.</para>
- /// </remarks>
- /// <seealso cref="DiscoveredSsdpDevice"/>
- /// <seealso cref="SsdpDevice"/>
- /// <seealso cref="ISsdpDevicePublisher"/>
- public interface ISsdpDeviceLocator
- {
- /// <summary>
- /// Event raised when a device becomes available or is found by a search request.
- /// </summary>
- /// <seealso cref="NotificationFilter"/>
- /// <seealso cref="DeviceUnavailable"/>
- /// <seealso cref="StartListeningForNotifications"/>
- /// <seealso cref="StopListeningForNotifications"/>
- event EventHandler<DeviceAvailableEventArgs> DeviceAvailable;
-
- /// <summary>
- /// Event raised when a device explicitly notifies of shutdown or a device expires from the cache.
- /// </summary>
- /// <seeseealso cref="NotificationFilter"/>
- /// <seealso cref="DeviceAvailable"/>
- /// <seealso cref="StartListeningForNotifications"/>
- /// <seealso cref="StopListeningForNotifications"/>
- event EventHandler<DeviceUnavailableEventArgs> DeviceUnavailable;
-
- /// <summary>
- /// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the <see cref="DeviceAvailable"/> or <see cref="DeviceUnavailable"/> events.
- /// </summary>
- /// <remarks>
- /// <para>Device alive/byebye notifications whose NT header does not match this filter value will still be captured and cached internally, but will not raise events about device availability. Usually used with either a device type of uuid NT header value.</para>
- /// <para>Example filters follow;</para>
- /// <example>upnp:rootdevice</example>
- /// <example>urn:schemas-upnp-org:device:WANDevice:1</example>
- /// <example>"uuid:9F15356CC-95FA-572E-0E99-85B456BD3012"</example>
- /// </remarks>
- /// <seealso cref="DeviceAvailable"/>
- /// <seealso cref="DeviceUnavailable"/>
- /// <seealso cref="StartListeningForNotifications"/>
- /// <seealso cref="StopListeningForNotifications"/>
- string NotificationFilter
- {
- get;
- set;
- }
-
- /// <summary>
- /// 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();
-
- /// <summary>
- /// Performs a search for the specified search target (criteria) and default search timeout.
- /// </summary>
- /// <param name="searchTarget">The criteria for the search. Value can be;
- /// <list type="table">
- /// <item><term>Root devices</term><description>upnp:rootdevice</description></item>
- /// <item><term>Specific device by UUID</term><description>uuid:&lt;device uuid&gt;</description></item>
- /// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item>
- /// </list>
- /// </param>
- /// <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);
-
- /// <summary>
- /// Performs a search for the specified search target (criteria) and search timeout.
- /// </summary>
- /// <param name="searchTarget">The criteria for the search. Value can be;
- /// <list type="table">
- /// <item><term>Root devices</term><description>upnp:rootdevice</description></item>
- /// <item><term>Specific device by UUID</term><description>uuid:&lt;device uuid&gt;</description></item>
- /// <item><term>Device type</term><description>A device namespace and type in format of urn:&lt;device namespace&gt;:device:&lt;device type&gt;:&lt;device version&gt; i.e urn:schemas-upnp-org:device:Basic:1</description></item>
- /// <item><term>Service type</term><description>A service namespace and type in format of urn:&lt;service namespace&gt;:service:&lt;servicetype&gt;:&lt;service version&gt; i.e urn:my-namespace:service:MyCustomService:1</description></item>
- /// </list>
- /// </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 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);
-
- /// <summary>
- /// Performs a search for all devices using the specified search timeout.
- /// </summary>
- /// <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>
- /// <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(TimeSpan searchWaitTime);
-
- /// <summary>
- /// Starts listening for broadcast notifications of service availability.
- /// </summary>
- /// <remarks>
- /// <para>When called the system will listen for 'alive' and 'byebye' notifications. This can speed up searching, as well as provide dynamic notification of new devices appearing on the network, and previously discovered devices disappearing.</para>
- /// </remarks>
- /// <seealso cref="StopListeningForNotifications"/>
- /// <seealso cref="DeviceAvailable"/>
- /// <seealso cref="DeviceUnavailable"/>
- /// <seealso cref="NotificationFilter"/>
- void StartListeningForNotifications();
-
- /// <summary>
- /// Stops listening for broadcast notifications of service availability.
- /// </summary>
- /// <remarks>
- /// <para>Does nothing if this instance is not already listening for notifications.</para>
- /// </remarks>
- /// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true.</exception>
- /// <seealso cref="StartListeningForNotifications"/>
- /// <seealso cref="DeviceAvailable"/>
- /// <seealso cref="DeviceUnavailable"/>
- /// <seealso cref="NotificationFilter"/>
- void StopListeningForNotifications();
- }
-}
diff --git a/RSSDP/ISsdpDevicePublisher.cs b/RSSDP/ISsdpDevicePublisher.cs
deleted file mode 100644
index 96c15443d..000000000
--- a/RSSDP/ISsdpDevicePublisher.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Threading.Tasks;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Interface for components that publish the existence of SSDP devices.
- /// </summary>
- /// <remarks>
- /// <para>Publishing a device includes sending notifications (alive and byebye) as well as responding to search requests when appropriate.</para>
- /// </remarks>
- /// <seealso cref="SsdpRootDevice"/>
- /// <seealso cref="ISsdpDeviceLocator"/>
- public interface ISsdpDevicePublisher
- {
- /// <summary>
- /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients.
- /// </summary>
- /// <param name="device">The <see cref="SsdpRootDevice"/> instance to add.</param>
- /// <returns>An awaitable <see cref="Task"/>.</returns>
- void AddDevice(SsdpRootDevice device);
-
- /// <summary>
- /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable.
- /// </summary>
- /// <param name="device">The <see cref="SsdpRootDevice"/> instance to add.</param>
- /// <returns>An awaitable <see cref="Task"/>.</returns>
- Task RemoveDevice(SsdpRootDevice device);
-
- /// <summary>
- /// Returns a read only list of devices being published by this instance.
- /// </summary>
- /// <seealso cref="SsdpDevice"/>
- System.Collections.Generic.IEnumerable<SsdpRootDevice> Devices { get; }
- }
-}
diff --git a/RSSDP/LICENSE b/RSSDP/LICENSE
deleted file mode 100644
index aabeb93af..000000000
--- a/RSSDP/LICENSE
+++ /dev/null
@@ -1,4 +0,0 @@
-RSSDP
-
-Copyright (c) 2015 Troy Willmot
-Copyright (c) 2015-2018 Luke Pulverenti
diff --git a/RSSDP/Properties/AssemblyInfo.cs b/RSSDP/Properties/AssemblyInfo.cs
deleted file mode 100644
index 55f7b6a83..000000000
--- a/RSSDP/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("RSSDP")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Jellyfin Project")]
-[assembly: AssemblyProduct("Jellyfin Server")]
-[assembly: AssemblyCopyright("Copyright © 2015 Troy Willmot. Code released under the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-[assembly: AssemblyVersion("1.0.3.0")]
-[assembly: AssemblyFileVersion("2019.1.20.3")]
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
deleted file mode 100644
index df5d982f6..000000000
--- a/RSSDP/RSSDP.csproj
+++ /dev/null
@@ -1,21 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
- <PropertyGroup>
- <ProjectGuid>{21002819-C39A-4D3E-BE83-2A276A77FB1F}</ProjectGuid>
- </PropertyGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\Jellyfin.Networking\Jellyfin.Networking.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
- </ItemGroup>
-
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <AnalysisMode>AllDisabledByDefault</AnalysisMode>
- <Nullable>disable</Nullable>
- <NoWarn>CA2016</NoWarn>
- </PropertyGroup>
-
-</Project>
diff --git a/RSSDP/RequestReceivedEventArgs.cs b/RSSDP/RequestReceivedEventArgs.cs
deleted file mode 100644
index b8b2249e4..000000000
--- a/RSSDP/RequestReceivedEventArgs.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-using System.Net;
-using System.Net.Http;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Provides arguments for the <see cref="ISsdpCommunicationsServer.RequestReceived"/> event.
- /// </summary>
- public sealed class RequestReceivedEventArgs : EventArgs
- {
- private readonly HttpRequestMessage _Message;
-
- private readonly IPEndPoint _ReceivedFrom;
-
- public IPAddress LocalIPAddress { get; private set; }
-
- /// <summary>
- /// Full constructor.
- /// </summary>
- public RequestReceivedEventArgs(HttpRequestMessage message, IPEndPoint receivedFrom, IPAddress localIPAddress)
- {
- _Message = message;
- _ReceivedFrom = receivedFrom;
- LocalIPAddress = localIPAddress;
- }
-
- /// <summary>
- /// The <see cref="HttpRequestMessage"/> that was received.
- /// </summary>
- public HttpRequestMessage Message
- {
- get { return _Message; }
- }
-
- /// <summary>
- /// The <see cref="IPEndPoint"/> the request came from.
- /// </summary>
- public IPEndPoint ReceivedFrom
- {
- get { return _ReceivedFrom; }
- }
- }
-}
diff --git a/RSSDP/ResponseReceivedEventArgs.cs b/RSSDP/ResponseReceivedEventArgs.cs
deleted file mode 100644
index e87ba1452..000000000
--- a/RSSDP/ResponseReceivedEventArgs.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.Net;
-using System.Net.Http;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Provides arguments for the <see cref="ISsdpCommunicationsServer.ResponseReceived"/> event.
- /// </summary>
- public sealed class ResponseReceivedEventArgs : EventArgs
- {
- public IPAddress LocalIPAddress { get; set; }
-
- private readonly HttpResponseMessage _Message;
-
- private readonly IPEndPoint _ReceivedFrom;
-
- /// <summary>
- /// Full constructor.
- /// </summary>
- public ResponseReceivedEventArgs(HttpResponseMessage message, IPEndPoint receivedFrom)
- {
- _Message = message;
- _ReceivedFrom = receivedFrom;
- }
-
- /// <summary>
- /// The <see cref="HttpResponseMessage"/> that was received.
- /// </summary>
- public HttpResponseMessage Message
- {
- get { return _Message; }
- }
-
- /// <summary>
- /// The <see cref="IPEndPoint"/> the response came from.
- /// </summary>
- public IPEndPoint ReceivedFrom
- {
- get { return _ReceivedFrom; }
- }
- }
-}
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
deleted file mode 100644
index a3f30c174..000000000
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ /dev/null
@@ -1,529 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Net.Sockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Provides the platform independent logic for publishing device existence and responding to search requests.
- /// </summary>
- public sealed class SsdpCommunicationsServer : DisposableManagedObjectBase, ISsdpCommunicationsServer
- {
- /* We could technically use one socket listening on port 1900 for everything.
- * This should get both multicast (notifications) and unicast (search response) messages, however
- * this often doesn't work under Windows because the MS SSDP service is running. If that service
- * is running then it will steal the unicast messages and we will never see search responses.
- * Since stopping the service would be a bad idea (might not be allowed security wise and might
- * break other apps running on the system) the only other work around is to use two sockets.
- *
- * We use one group of sockets to listen for/receive notifications and search requests (_MulticastListenSockets).
- * We use a second group, bound to a different local port, to send search requests and listen for
- * responses (_SendSockets). The responses are sent to the local ports these sockets are bound to,
- * which aren't port 1900 so the MS service doesn't steal them. While the caller can specify a local
- * port to use, we will default to 0 which allows the underlying system to auto-assign a free port.
- */
-
- private object _BroadcastListenSocketSynchroniser = new();
- private List<Socket> _MulticastListenSockets;
-
- private object _SendSocketSynchroniser = new();
- private List<Socket> _sendSockets;
-
- private HttpRequestParser _RequestParser;
- private HttpResponseParser _ResponseParser;
- private readonly ILogger _logger;
- private ISocketFactory _SocketFactory;
- private readonly INetworkManager _networkManager;
-
- private int _LocalPort;
- private int _MulticastTtl;
-
- private bool _IsShared;
-
- /// <summary>
- /// Raised when a HTTPU request message is received by a socket (unicast or multicast).
- /// </summary>
- public event EventHandler<RequestReceivedEventArgs> RequestReceived;
-
- /// <summary>
- /// Raised when an HTTPU response message is received by a socket (unicast or multicast).
- /// </summary>
- public event EventHandler<ResponseReceivedEventArgs> ResponseReceived;
-
- /// <summary>
- /// Minimum constructor.
- /// </summary>
- /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
- public SsdpCommunicationsServer(
- ISocketFactory socketFactory,
- INetworkManager networkManager,
- ILogger logger)
- : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger)
- {
-
- }
-
- /// <summary>
- /// Full constructor.
- /// </summary>
- /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
- /// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception>
- public SsdpCommunicationsServer(
- ISocketFactory socketFactory,
- int localPort,
- int multicastTimeToLive,
- INetworkManager networkManager,
- ILogger logger)
- {
- if (socketFactory is null)
- {
- throw new ArgumentNullException(nameof(socketFactory));
- }
-
- if (multicastTimeToLive <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero.");
- }
-
- _BroadcastListenSocketSynchroniser = new();
- _SendSocketSynchroniser = new();
-
- _LocalPort = localPort;
- _SocketFactory = socketFactory;
-
- _RequestParser = new();
- _ResponseParser = new();
-
- _MulticastTtl = multicastTimeToLive;
- _networkManager = networkManager;
- _logger = logger;
- }
-
- /// <summary>
- /// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications.
- /// </summary>
- /// <exception cref="ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception>
- public void BeginListeningForMulticast()
- {
- ThrowIfDisposed();
-
- lock (_BroadcastListenSocketSynchroniser)
- {
- if (_MulticastListenSockets is null)
- {
- try
- {
- _MulticastListenSockets = CreateMulticastSocketsAndListen();
- }
- catch (SocketException ex)
- {
- _logger.LogError("Failed to bind to multicast address: {Message}. DLNA will be unavailable", ex.Message);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in BeginListeningForMulticast");
- }
- }
- }
- }
-
- /// <summary>
- /// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications.
- /// </summary>
- /// <exception cref="ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception>
- public void StopListeningForMulticast()
- {
- lock (_BroadcastListenSocketSynchroniser)
- {
- if (_MulticastListenSockets is not null)
- {
- _logger.LogInformation("{0} disposing _BroadcastListenSocket", GetType().Name);
- foreach (var socket in _MulticastListenSockets)
- {
- socket.Dispose();
- }
-
- _MulticastListenSockets = null;
- }
- }
- }
-
- /// <summary>
- /// Sends a message to a particular address (uni or multicast) and port.
- /// </summary>
- public async Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromlocalIPAddress, CancellationToken cancellationToken)
- {
- if (messageData is null)
- {
- throw new ArgumentNullException(nameof(messageData));
- }
-
- ThrowIfDisposed();
-
- var sockets = GetSendSockets(fromlocalIPAddress, destination);
-
- if (sockets.Count == 0)
- {
- return;
- }
-
- // SSDP spec recommends sending messages multiple times (not more than 3) to account for possible packet loss over UDP.
- for (var i = 0; i < SsdpConstants.UdpResendCount; i++)
- {
- var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken)).ToArray();
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
- }
-
- private async Task SendFromSocket(Socket socket, byte[] messageData, IPEndPoint destination, CancellationToken cancellationToken)
- {
- try
- {
- await socket.SendToAsync(messageData, destination, cancellationToken).ConfigureAwait(false);
- }
- catch (ObjectDisposedException)
- {
- }
- catch (OperationCanceledException)
- {
- }
- catch (Exception ex)
- {
- var localIP = ((IPEndPoint)socket.LocalEndPoint).Address;
- _logger.LogError(ex, "Error sending socket message from {0} to {1}", localIP.ToString(), destination.ToString());
- }
- }
-
- private List<Socket> GetSendSockets(IPAddress fromlocalIPAddress, IPEndPoint destination)
- {
- EnsureSendSocketCreated();
-
- lock (_SendSocketSynchroniser)
- {
- var sockets = _sendSockets.Where(s => s.AddressFamily == fromlocalIPAddress.AddressFamily);
-
- // Send from the Any socket and the socket with the matching address
- if (fromlocalIPAddress.AddressFamily == AddressFamily.InterNetwork)
- {
- sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Any)
- || ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromlocalIPAddress));
-
- // If sending to the loopback address, filter the socket list as well
- if (destination.Address.Equals(IPAddress.Loopback))
- {
- sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Any)
- || ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Loopback));
- }
- }
- else if (fromlocalIPAddress.AddressFamily == AddressFamily.InterNetworkV6)
- {
- sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Any)
- || ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromlocalIPAddress));
-
- // If sending to the loopback address, filter the socket list as well
- if (destination.Address.Equals(IPAddress.IPv6Loopback))
- {
- sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Any)
- || ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Loopback));
- }
- }
-
- return sockets.ToList();
- }
- }
-
- public Task SendMulticastMessage(string message, IPAddress fromlocalIPAddress, CancellationToken cancellationToken)
- {
- return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromlocalIPAddress, cancellationToken);
- }
-
- /// <summary>
- /// Sends a message to the SSDP multicast address and port.
- /// </summary>
- public async Task SendMulticastMessage(string message, int sendCount, IPAddress fromlocalIPAddress, CancellationToken cancellationToken)
- {
- if (message is null)
- {
- throw new ArgumentNullException(nameof(message));
- }
-
- byte[] messageData = Encoding.UTF8.GetBytes(message);
-
- ThrowIfDisposed();
-
- cancellationToken.ThrowIfCancellationRequested();
-
- EnsureSendSocketCreated();
-
- // SSDP spec recommends sending messages multiple times (not more than 3) to account for possible packet loss over UDP.
- for (var i = 0; i < sendCount; i++)
- {
- await SendMessageIfSocketNotDisposed(
- messageData,
- new IPEndPoint(
- IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress),
- SsdpConstants.MulticastPort),
- fromlocalIPAddress,
- cancellationToken).ConfigureAwait(false);
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Stops listening for search responses on the local, unicast socket.
- /// </summary>
- /// <exception cref="ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception>
- public void StopListeningForResponses()
- {
- lock (_SendSocketSynchroniser)
- {
- if (_sendSockets is not null)
- {
- var sockets = _sendSockets.ToList();
- _sendSockets = null;
-
- _logger.LogInformation("{0} Disposing {1} sendSockets", GetType().Name, sockets.Count);
-
- foreach (var socket in sockets)
- {
- var socketAddress = ((IPEndPoint)socket.LocalEndPoint).Address;
- _logger.LogInformation("{0} disposing sendSocket from {1}", GetType().Name, socketAddress);
- socket.Dispose();
- }
- }
- }
- }
-
- /// <summary>
- /// Gets or sets a boolean value indicating whether or not this instance is shared amongst multiple <see cref="SsdpDeviceLocator"/> and/or <see cref="ISsdpDevicePublisher"/> instances.
- /// </summary>
- /// <remarks>
- /// <para>If true, disposing an instance of a <see cref="SsdpDeviceLocator"/>or a <see cref="ISsdpDevicePublisher"/> will not dispose this comms server instance. The calling code is responsible for managing the lifetime of the server.</para>
- /// </remarks>
- public bool IsShared
- {
- get { return _IsShared; }
-
- set { _IsShared = value; }
- }
-
- /// <summary>
- /// Stops listening for requests, disposes this instance and all internal resources.
- /// </summary>
- /// <param name="disposing"></param>
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- StopListeningForMulticast();
-
- StopListeningForResponses();
- }
- }
-
- private Task SendMessageIfSocketNotDisposed(byte[] messageData, IPEndPoint destination, IPAddress fromlocalIPAddress, CancellationToken cancellationToken)
- {
- var sockets = _sendSockets;
- if (sockets is not null)
- {
- sockets = sockets.ToList();
-
- var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))
- .Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
- return Task.WhenAll(tasks);
- }
-
- return Task.CompletedTask;
- }
-
- private List<Socket> CreateMulticastSocketsAndListen()
- {
- var sockets = new List<Socket>();
- var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress);
-
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses()
- .Where(x => x.Address is not null)
- .Where(x => x.SupportsMulticast)
- .Where(x => x.AddressFamily == AddressFamily.InterNetwork)
- .DistinctBy(x => x.Index);
-
- foreach (var intf in validInterfaces)
- {
- try
- {
- var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
- }
- }
-
- return sockets;
- }
-
- private List<Socket> CreateSendSockets()
- {
- var sockets = new List<Socket>();
-
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses()
- .Where(x => x.Address is not null)
- .Where(x => x.SupportsMulticast)
- .Where(x => x.AddressFamily == AddressFamily.InterNetwork);
-
- if (OperatingSystem.IsMacOS())
- {
- // Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340
- validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback));
- }
-
- foreach (var intf in validInterfaces)
- {
- try
- {
- var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
- }
- }
-
- return sockets;
- }
-
- private async Task ListenToSocketInternal(Socket socket)
- {
- var cancelled = false;
- var receiveBuffer = new byte[8192];
-
- while (!cancelled && !IsDisposed)
- {
- try
- {
- var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);;
-
- if (result.ReceivedBytes > 0)
- {
- var remoteEndpoint = (IPEndPoint)result.RemoteEndPoint;
- var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface);
-
- ProcessMessage(
- Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
- remoteEndpoint,
- localEndpointAdapter.Address);
- }
- }
- catch (ObjectDisposedException)
- {
- cancelled = true;
- }
- catch (TaskCanceledException)
- {
- cancelled = true;
- }
- }
- }
-
- private void EnsureSendSocketCreated()
- {
- if (_sendSockets is null)
- {
- lock (_SendSocketSynchroniser)
- {
- _sendSockets ??= CreateSendSockets();
- }
- }
- }
-
- private void ProcessMessage(string data, IPEndPoint endPoint, IPAddress receivedOnlocalIPAddress)
- {
- // Responses start with the HTTP version, prefixed with HTTP/ while
- // requests start with a method which can vary and might be one we haven't
- // seen/don't know. We'll check if this message is a request or a response
- // by checking for the HTTP/ prefix on the start of the message.
- _logger.LogDebug("Received data from {From} on {Port} at {Address}:\n{Data}", endPoint.Address, endPoint.Port, receivedOnlocalIPAddress, data);
- if (data.StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase))
- {
- HttpResponseMessage responseMessage = null;
- try
- {
- responseMessage = _ResponseParser.Parse(data);
- }
- catch (ArgumentException)
- {
- // Ignore invalid packets.
- }
-
- if (responseMessage is not null)
- {
- OnResponseReceived(responseMessage, endPoint, receivedOnlocalIPAddress);
- }
- }
- else
- {
- HttpRequestMessage requestMessage = null;
- try
- {
- requestMessage = _RequestParser.Parse(data);
- }
- catch (ArgumentException)
- {
- // Ignore invalid packets.
- }
-
- if (requestMessage is not null)
- {
- OnRequestReceived(requestMessage, endPoint, receivedOnlocalIPAddress);
- }
- }
- }
-
- private void OnRequestReceived(HttpRequestMessage data, IPEndPoint remoteEndPoint, IPAddress receivedOnlocalIPAddress)
- {
- // SSDP specification says only * is currently used but other uri's might
- // be implemented in the future and should be ignored unless understood.
- // Section 4.2 - http://tools.ietf.org/html/draft-cai-ssdp-v1-03#page-11
- if (data.RequestUri.ToString() != "*")
- {
- return;
- }
-
- var handlers = RequestReceived;
- if (handlers is not null)
- {
- handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress));
- }
- }
-
- private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress)
- {
- var handlers = ResponseReceived;
- if (handlers is not null)
- {
- handlers(this, new ResponseReceivedEventArgs(data, endPoint)
- {
- LocalIPAddress = localIPAddress
- });
- }
- }
- }
-}
diff --git a/RSSDP/SsdpConstants.cs b/RSSDP/SsdpConstants.cs
deleted file mode 100644
index 442f2b8f8..000000000
--- a/RSSDP/SsdpConstants.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Provides constants for common values related to the SSDP protocols.
- /// </summary>
- public static class SsdpConstants
- {
-
- /// <summary>
- /// Multicast IP Address used for SSDP multicast messages. Values is 239.255.255.250.
- /// </summary>
- public const string MulticastLocalAdminAddress = "239.255.255.250";
- /// <summary>
- /// The UDP port used for SSDP multicast messages. Values is 1900.
- /// </summary>
- public const int MulticastPort = 1900;
- /// <summary>
- /// The default multicase TTL for SSDP multicast messages. Value is 4.
- /// </summary>
- public const int SsdpDefaultMulticastTimeToLive = 4;
-
- internal const string MSearchMethod = "M-SEARCH";
-
- internal const string SsdpDiscoverMessage = "ssdp:discover";
- internal const string SsdpDiscoverAllSTHeader = "ssdp:all";
-
- internal const string SsdpDeviceDescriptionXmlNamespace = "urn:schemas-upnp-org:device-1-0";
-
- internal const string ServerVersion = "1.0";
-
- /// <summary>
- /// Default buffer size for receiving SSDP broadcasts. Value is 8192 (bytes).
- /// </summary>
- public const int DefaultUdpSocketBufferSize = 8192;
- /// <summary>
- /// The maximum possible buffer size for a UDP message. Value is 65507 (bytes).
- /// </summary>
- public const int MaxUdpSocketBufferSize = 65507; // Max possible UDP packet size on IPv4 without using 'jumbograms'.
-
- /// <summary>
- /// Namespace/prefix for UPnP device types. Values is schemas-upnp-org.
- /// </summary>
- public const string UpnpDeviceTypeNamespace = "schemas-upnp-org";
- /// <summary>
- /// UPnP Root Device type. Value is upnp:rootdevice.
- /// </summary>
- public const string UpnpDeviceTypeRootDevice = "upnp:rootdevice";
- /// <summary>
- /// The value is used by Windows Explorer for device searches instead of the UPNPDeviceTypeRootDevice constant.
- /// Not sure why (different spec, bug, alternate protocol etc). Used to enable Windows Explorer support.
- /// </summary>
- public const string PnpDeviceTypeRootDevice = "pnp:rootdevice";
- /// <summary>
- /// UPnP Basic Device type. Value is Basic.
- /// </summary>
- public const string UpnpDeviceTypeBasicDevice = "Basic";
-
- internal const string SsdpKeepAliveNotification = "ssdp:alive";
- internal const string SsdpByeByeNotification = "ssdp:byebye";
-
- internal const int UdpResendCount = 3;
- }
-}
diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs
deleted file mode 100644
index 3e4261b6a..000000000
--- a/RSSDP/SsdpDevice.cs
+++ /dev/null
@@ -1,361 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Globalization;
-using Rssdp.Infrastructure;
-
-namespace Rssdp
-{
- /// <summary>
- /// Base class representing the common details of a (root or embedded) device, either to be published or that has been located.
- /// </summary>
- /// <remarks>
- /// <para>Do not derive new types directly from this class. New device classes should derive from either <see cref="SsdpRootDevice"/> or <see cref="SsdpEmbeddedDevice"/>.</para>
- /// </remarks>
- /// <seealso cref="SsdpRootDevice"/>
- /// <seealso cref="SsdpEmbeddedDevice"/>
- public abstract class SsdpDevice
- {
- private string _Udn;
- private string _DeviceType;
- private string _DeviceTypeNamespace;
- private int _DeviceVersion;
-
- private IList<SsdpDevice> _Devices;
-
- /// <summary>
- /// Raised when a new child device is added.
- /// </summary>
- /// <seealso cref="AddDevice"/>
- /// <seealso cref="DeviceAdded"/>
- public event EventHandler<DeviceEventArgs> DeviceAdded;
-
- /// <summary>
- /// Raised when a child device is removed.
- /// </summary>
- /// <seealso cref="RemoveDevice"/>
- /// <seealso cref="DeviceRemoved"/>
- public event EventHandler<DeviceEventArgs> DeviceRemoved;
-
- /// <summary>
- /// Derived type constructor, allows constructing a device with no parent. Should only be used from derived types that are or inherit from <see cref="SsdpRootDevice"/>.
- /// </summary>
- protected SsdpDevice()
- {
- _DeviceTypeNamespace = SsdpConstants.UpnpDeviceTypeNamespace;
- _DeviceType = SsdpConstants.UpnpDeviceTypeBasicDevice;
- _DeviceVersion = 1;
-
- _Devices = new List<SsdpDevice>();
- this.Devices = new ReadOnlyCollection<SsdpDevice>(_Devices);
- }
-
- public SsdpRootDevice ToRootDevice()
- {
- var device = this;
-
- var rootDevice = device as SsdpRootDevice;
- if (rootDevice == null)
- {
- rootDevice = ((SsdpEmbeddedDevice)device).RootDevice;
- }
-
- return rootDevice;
- }
-
- /// <summary>
- /// Sets or returns the core device type (not including namespace, version etc.). Required.
- /// </summary>
- /// <remarks><para>Defaults to the UPnP basic device type.</para></remarks>
- /// <seealso cref="DeviceTypeNamespace"/>
- /// <seealso cref="DeviceVersion"/>
- /// <seealso cref="FullDeviceType"/>
- public string DeviceType
- {
- get
- {
- return _DeviceType;
- }
-
- set
- {
- _DeviceType = value;
- }
- }
-
- public string DeviceClass { get; set; }
-
- /// <summary>
- /// Sets or returns the namespace for the <see cref="DeviceType"/> of this device. Optional, but defaults to UPnP schema so should be changed if <see cref="DeviceType"/> is not a UPnP device type.
- /// </summary>
- /// <remarks><para>Defaults to the UPnP standard namespace.</para></remarks>
- /// <seealso cref="DeviceType"/>
- /// <seealso cref="DeviceVersion"/>
- /// <seealso cref="FullDeviceType"/>
- public string DeviceTypeNamespace
- {
- get
- {
- return _DeviceTypeNamespace;
- }
-
- set
- {
- _DeviceTypeNamespace = value;
- }
- }
-
- /// <summary>
- /// Sets or returns the version of the device type. Optional, defaults to 1.
- /// </summary>
- /// <remarks><para>Defaults to a value of 1.</para></remarks>
- /// <seealso cref="DeviceType"/>
- /// <seealso cref="DeviceTypeNamespace"/>
- /// <seealso cref="FullDeviceType"/>
- public int DeviceVersion
- {
- get
- {
- return _DeviceVersion;
- }
-
- set
- {
- _DeviceVersion = value;
- }
- }
-
- /// <summary>
- /// Returns the full device type string.
- /// </summary>
- /// <remarks>
- /// <para>The format used is urn:<see cref="DeviceTypeNamespace"/>:device:<see cref="DeviceType"/>:<see cref="DeviceVersion"/></para>
- /// </remarks>
- public string FullDeviceType
- {
- get
- {
- return String.Format(
- CultureInfo.InvariantCulture,
- "urn:{0}:{3}:{1}:{2}",
- this.DeviceTypeNamespace ?? String.Empty,
- this.DeviceType ?? String.Empty,
- this.DeviceVersion,
- this.DeviceClass ?? "device");
- }
- }
-
- /// <summary>
- /// Sets or returns the universally unique identifier for this device (without the uuid: prefix). Required.
- /// </summary>
- /// <remarks>
- /// <para>Must be the same over time for a specific device instance (i.e. must survive reboots).</para>
- /// <para>For UPnP 1.0 this can be any unique string. For UPnP 1.1 this should be a 128 bit number formatted in a specific way, preferably generated using the time and MAC based algorithm. See section 1.1.4 of http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf for details.</para>
- /// <para>Technically this library implements UPnP 1.0, so any value is allowed, but we advise using UPnP 1.1 compatible values for good behaviour and forward compatibility with future versions.</para>
- /// </remarks>
- public string Uuid { get; set; }
-
- /// <summary>
- /// Returns (or sets*) a unique device name for this device. Optional, not recommended to be explicitly set.
- /// </summary>
- /// <remarks>
- /// <para>* In general you should not explicitly set this property. If it is not set (or set to null/empty string) the property will return a UDN value that is correct as per the UPnP specification, based on the other device properties.</para>
- /// <para>The setter is provided to allow for devices that do not correctly follow the specification (when we discover them), rather than to intentionally deviate from the specification.</para>
- /// <para>If a value is explicitly set, it is used verbatim, and so any prefix (such as uuid:) must be provided in the value.</para>
- /// </remarks>
- public string Udn
- {
- get
- {
- if (String.IsNullOrEmpty(_Udn) && !String.IsNullOrEmpty(this.Uuid))
- {
- return "uuid:" + this.Uuid;
- }
-
- return _Udn;
- }
-
- set
- {
- _Udn = value;
- }
- }
-
- /// <summary>
- /// Sets or returns a friendly/display name for this device on the network. Something the user can identify the device/instance by, i.e Lounge Main Light. Required.
- /// </summary>
- /// <remarks><para>A short description for the end user. </para></remarks>
- public string FriendlyName { get; set; }
-
- /// <summary>
- /// Sets or returns the name of the manufacturer of this device. Required.
- /// </summary>
- public string Manufacturer { get; set; }
-
- /// <summary>
- /// Sets or returns a URL to the manufacturers web site. Optional.
- /// </summary>
- public Uri ManufacturerUrl { get; set; }
-
- /// <summary>
- /// Sets or returns a description of this device model. Recommended.
- /// </summary>
- /// <remarks><para>A long description for the end user.</para></remarks>
- public string ModelDescription { get; set; }
-
- /// <summary>
- /// Sets or returns the name of this model. Required.
- /// </summary>
- public string ModelName { get; set; }
-
- /// <summary>
- /// Sets or returns the number of this model. Recommended.
- /// </summary>
- public string ModelNumber { get; set; }
-
- /// <summary>
- /// Sets or returns a URL to a web page with details of this device model. Optional.
- /// </summary>
- /// <remarks>
- /// <para>Optional. May be relative to base URL.</para>
- /// </remarks>
- public Uri ModelUrl { get; set; }
-
- /// <summary>
- /// Sets or returns the serial number for this device. Recommended.
- /// </summary>
- public string SerialNumber { get; set; }
-
- /// <summary>
- /// Sets or returns the universal product code of the device, if any. Optional.
- /// </summary>
- /// <remarks>
- /// <para>If not blank, must be exactly 12 numeric digits.</para>
- /// </remarks>
- public string Upc { get; set; }
-
- /// <summary>
- /// Sets or returns the URL to a web page that can be used to configure/manager/use the device. Recommended.
- /// </summary>
- /// <remarks>
- /// <para>May be relative to base URL. </para>
- /// </remarks>
- public Uri PresentationUrl { get; set; }
-
- /// <summary>
- /// Returns a read-only enumerable set of <see cref="SsdpDevice"/> objects representing children of this device. Child devices are optional.
- /// </summary>
- /// <seealso cref="AddDevice"/>
- /// <seealso cref="RemoveDevice"/>
- public IList<SsdpDevice> Devices
- {
- get;
- private set;
- }
-
- /// <summary>
- /// Adds a child device to the <see cref="Devices"/> collection.
- /// </summary>
- /// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance to add.</param>
- /// <remarks>
- /// <para>If the device is already a member of the <see cref="Devices"/> collection, this method does nothing.</para>
- /// <para>Also sets the <see cref="SsdpEmbeddedDevice.RootDevice"/> property of the added device and all descendant devices to the relevant <see cref="SsdpRootDevice"/> instance.</para>
- /// </remarks>
- /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
- /// <exception cref="InvalidOperationException">Thrown if the <paramref name="device"/> is already associated with a different <see cref="SsdpRootDevice"/> instance than used in this tree. Can occur if you try to add the same device instance to more than one tree. Also thrown if you try to add a device to itself.</exception>
- /// <seealso cref="DeviceAdded"/>
- public void AddDevice(SsdpEmbeddedDevice device)
- {
- if (device == null)
- {
- throw new ArgumentNullException(nameof(device));
- }
-
- if (device.RootDevice != null && device.RootDevice != this.ToRootDevice())
- {
- throw new InvalidOperationException("This device is already associated with a different root device (has been added as a child in another branch).");
- }
-
- if (device == this)
- {
- throw new InvalidOperationException("Can't add device to itself.");
- }
-
- bool wasAdded = false;
- lock (_Devices)
- {
- device.RootDevice = this.ToRootDevice();
- _Devices.Add(device);
- wasAdded = true;
- }
-
- if (wasAdded)
- {
- OnDeviceAdded(device);
- }
- }
-
- /// <summary>
- /// Removes a child device from the <see cref="Devices"/> collection.
- /// </summary>
- /// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance to remove.</param>
- /// <remarks>
- /// <para>If the device is not a member of the <see cref="Devices"/> collection, this method does nothing.</para>
- /// <para>Also sets the <see cref="SsdpEmbeddedDevice.RootDevice"/> property to null for the removed device and all descendant devices.</para>
- /// </remarks>
- /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
- /// <seealso cref="DeviceRemoved"/>
- public void RemoveDevice(SsdpEmbeddedDevice device)
- {
- if (device == null)
- {
- throw new ArgumentNullException(nameof(device));
- }
-
- bool wasRemoved = false;
- lock (_Devices)
- {
- wasRemoved = _Devices.Remove(device);
- if (wasRemoved)
- {
- device.RootDevice = null;
- }
- }
-
- if (wasRemoved)
- {
- OnDeviceRemoved(device);
- }
- }
-
- /// <summary>
- /// Raises the <see cref="DeviceAdded"/> event.
- /// </summary>
- /// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance added to the <see cref="Devices"/> collection.</param>
- /// <seealso cref="AddDevice"/>
- /// <seealso cref="DeviceAdded"/>
- protected virtual void OnDeviceAdded(SsdpEmbeddedDevice device)
- {
- var handlers = this.DeviceAdded;
- if (handlers != null)
- {
- handlers(this, new DeviceEventArgs(device));
- }
- }
-
- /// <summary>
- /// Raises the <see cref="DeviceRemoved"/> event.
- /// </summary>
- /// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance removed from the <see cref="Devices"/> collection.</param>
- /// <seealso cref="RemoveDevice"/>
- /// <see cref="DeviceRemoved"/>
- protected virtual void OnDeviceRemoved(SsdpEmbeddedDevice device)
- {
- var handlers = this.DeviceRemoved;
- if (handlers != null)
- {
- handlers(this, new DeviceEventArgs(device));
- }
- }
- }
-}
diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs
deleted file mode 100644
index 82b09c4b4..000000000
--- a/RSSDP/SsdpDeviceLocator.cs
+++ /dev/null
@@ -1,632 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Allows you to search the network for a particular device, device types, or UPnP service types. Also listenings for broadcast notifications of device availability and raises events to indicate changes in status.
- /// </summary>
- public class SsdpDeviceLocator : DisposableManagedObjectBase
- {
- private List<DiscoveredSsdpDevice> _Devices;
- private ISsdpCommunicationsServer _CommunicationsServer;
-
- private Timer _BroadcastTimer;
- private object _timerLock = new();
-
- private string _OSName;
-
- private string _OSVersion;
-
- private readonly TimeSpan DefaultSearchWaitTime = TimeSpan.FromSeconds(4);
- private readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1);
-
- /// <summary>
- /// Default constructor.
- /// </summary>
- public SsdpDeviceLocator(
- ISsdpCommunicationsServer communicationsServer,
- string osName,
- string osVersion)
- {
- ArgumentNullException.ThrowIfNull(communicationsServer);
- ArgumentNullException.ThrowIfNullOrEmpty(osName);
- ArgumentNullException.ThrowIfNullOrEmpty(osVersion);
-
- _OSName = osName;
- _OSVersion = osVersion;
- _CommunicationsServer = communicationsServer;
- _CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived;
-
- _Devices = new List<DiscoveredSsdpDevice>();
- }
-
- /// <summary>
- /// Raised for when
- /// <list type="bullet">
- /// <item>An 'alive' notification is received that a device, regardless of whether or not that device is not already in the cache or has previously raised this event.</item>
- /// <item>For each item found during a device <see cref="SearchAsync(System.Threading.CancellationToken)"/> (cached or not), allowing clients to respond to found devices before the entire search is complete.</item>
- /// <item>Only if the notification type matches the <see cref="NotificationFilter"/> property. By default the filter is null, meaning all notifications raise events (regardless of ant </item>
- /// </list>
- /// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para>
- /// </summary>
- /// <seealso cref="NotificationFilter"/>
- /// <seealso cref="DeviceUnavailable"/>
- /// <seealso cref="StartListeningForNotifications"/>
- /// <seealso cref="StopListeningForNotifications"/>
- public event EventHandler<DeviceAvailableEventArgs> DeviceAvailable;
-
- /// <summary>
- /// Raised when a notification is received that indicates a device has shutdown or otherwise become unavailable.
- /// </summary>
- /// <remarks>
- /// <para>Devices *should* broadcast these types of notifications, but not all devices do and sometimes (in the event of power loss for example) it might not be possible for a device to do so. You should also implement error handling when trying to contact a device, even if RSSDP is reporting that device as available.</para>
- /// <para>This event is only raised if the notification type matches the <see cref="NotificationFilter"/> property. A null or empty string for the <see cref="NotificationFilter"/> will be treated as no filter and raise the event for all notifications.</para>
- /// <para>The <see cref="DeviceUnavailableEventArgs.DiscoveredDevice"/> property may contain either a fully complete <see cref="DiscoveredSsdpDevice"/> instance, or one containing just a USN and NotificationType property. Full information is available if the device was previously discovered and cached, but only partial information if a byebye notification was received for a previously unseen or expired device.</para>
- /// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para>
- /// </remarks>
- /// <seealso cref="NotificationFilter"/>
- /// <seealso cref="DeviceAvailable"/>
- /// <seealso cref="StartListeningForNotifications"/>
- /// <seealso cref="StopListeningForNotifications"/>
- public event EventHandler<DeviceUnavailableEventArgs> DeviceUnavailable;
-
- public void RestartBroadcastTimer(TimeSpan dueTime, TimeSpan period)
- {
- lock (_timerLock)
- {
- if (_BroadcastTimer is null)
- {
- _BroadcastTimer = new Timer(OnBroadcastTimerCallback, null, dueTime, period);
- }
- else
- {
- _BroadcastTimer.Change(dueTime, period);
- }
- }
- }
-
- public void DisposeBroadcastTimer()
- {
- lock (_timerLock)
- {
- if (_BroadcastTimer is not null)
- {
- _BroadcastTimer.Dispose();
- _BroadcastTimer = null;
- }
- }
- }
-
- private async void OnBroadcastTimerCallback(object state)
- {
- if (IsDisposed)
- {
- return;
- }
-
- StartListeningForNotifications();
- RemoveExpiredDevicesFromCache();
-
- try
- {
- await SearchAsync(CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception)
- {
- }
- }
-
- /// <summary>
- /// Performs a search for all devices using the default search timeout.
- /// </summary>
- private Task SearchAsync(CancellationToken cancellationToken)
- {
- return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, DefaultSearchWaitTime, cancellationToken);
- }
-
- /// <summary>
- /// Performs a search for the specified search target (criteria) and default search timeout.
- /// </summary>
- /// <param name="searchTarget">The criteria for the search. Value can be;
- /// <list type="table">
- /// <item><term>Root devices</term><description>upnp:rootdevice</description></item>
- /// <item><term>Specific device by UUID</term><description>uuid:&lt;device uuid&gt;</description></item>
- /// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item>
- /// </list>
- /// </param>
- private Task SearchAsync(string searchTarget)
- {
- return SearchAsync(searchTarget, DefaultSearchWaitTime, CancellationToken.None);
- }
-
- /// <summary>
- /// Performs a search for all devices using the specified search timeout.
- /// </summary>
- /// <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 seconds is recommended by the UPnP 1.1 specification, this method requires the value be greater 1 second if it is not zero. Specify TimeSpan.Zero to return only devices already in the cache.</param>
- private Task SearchAsync(TimeSpan searchWaitTime)
- {
- return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, searchWaitTime, CancellationToken.None);
- }
-
- private Task SearchAsync(string searchTarget, TimeSpan searchWaitTime, CancellationToken cancellationToken)
- {
- if (searchTarget is null)
- {
- throw new ArgumentNullException(nameof(searchTarget));
- }
-
- if (searchTarget.Length == 0)
- {
- throw new ArgumentException("searchTarget cannot be an empty string.", nameof(searchTarget));
- }
-
- if (searchWaitTime.TotalSeconds < 0)
- {
- throw new ArgumentException("searchWaitTime must be a positive time.");
- }
-
- if (searchWaitTime.TotalSeconds > 0 && searchWaitTime.TotalSeconds <= 1)
- {
- throw new ArgumentException("searchWaitTime must be zero (if you are not using the result and relying entirely in the events), or greater than one second.");
- }
-
- ThrowIfDisposed();
-
- return BroadcastDiscoverMessage(searchTarget, SearchTimeToMXValue(searchWaitTime), cancellationToken);
- }
-
- /// <summary>
- /// Starts listening for broadcast notifications of service availability.
- /// </summary>
- /// <remarks>
- /// <para>When called the system will listen for 'alive' and 'byebye' notifications. This can speed up searching, as well as provide dynamic notification of new devices appearing on the network, and previously discovered devices disappearing.</para>
- /// </remarks>
- /// <seealso cref="StopListeningForNotifications"/>
- /// <seealso cref="DeviceAvailable"/>
- /// <seealso cref="DeviceUnavailable"/>
- /// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> ty is true.</exception>
- public void StartListeningForNotifications()
- {
- _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived;
- _CommunicationsServer.RequestReceived += CommsServer_RequestReceived;
- _CommunicationsServer.BeginListeningForMulticast();
- }
-
- /// <summary>
- /// Stops listening for broadcast notifications of service availability.
- /// </summary>
- /// <remarks>
- /// <para>Does nothing if this instance is not already listening for notifications.</para>
- /// </remarks>
- /// <seealso cref="StartListeningForNotifications"/>
- /// <seealso cref="DeviceAvailable"/>
- /// <seealso cref="DeviceUnavailable"/>
- /// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true.</exception>
- public void StopListeningForNotifications()
- {
- ThrowIfDisposed();
-
- _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived;
- }
-
- /// <summary>
- /// Raises the <see cref="DeviceAvailable"/> event.
- /// </summary>
- /// <seealso cref="DeviceAvailable"/>
- protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
- {
- if (IsDisposed)
- {
- return;
- }
-
- var handlers = DeviceAvailable;
- if (handlers is not null)
- {
- handlers(this, new DeviceAvailableEventArgs(device, isNewDevice)
- {
- RemoteIPAddress = IPAddress
- });
- }
- }
-
- /// <summary>
- /// Raises the <see cref="DeviceUnavailable"/> event.
- /// </summary>
- /// <param name="device">A <see cref="DiscoveredSsdpDevice"/> representing the device that is no longer available.</param>
- /// <param name="expired">True if the device expired from the cache without being renewed, otherwise false to indicate the device explicitly notified us it was being shutdown.</param>
- /// <seealso cref="DeviceUnavailable"/>
- protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired)
- {
- if (IsDisposed)
- {
- return;
- }
-
- var handlers = DeviceUnavailable;
- if (handlers is not null)
- {
- handlers(this, new DeviceUnavailableEventArgs(device, expired));
- }
- }
-
- /// <summary>
- /// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the <see cref="ISsdpDeviceLocator.DeviceAvailable"/> or <see cref="ISsdpDeviceLocator.DeviceUnavailable"/> events.
- /// </summary>
- /// <remarks>
- /// <para>Device alive/byebye notifications whose NT header does not match this filter value will still be captured and cached internally, but will not raise events about device availability. Usually used with either a device type of uuid NT header value.</para>
- /// <para>If the value is null or empty string then, all notifications are reported.</para>
- /// <para>Example filters follow;</para>
- /// <example>upnp:rootdevice</example>
- /// <example>urn:schemas-upnp-org:device:WANDevice:1</example>
- /// <example>uuid:9F15356CC-95FA-572E-0E99-85B456BD3012</example>
- /// </remarks>
- /// <seealso cref="ISsdpDeviceLocator.DeviceAvailable"/>
- /// <seealso cref="ISsdpDeviceLocator.DeviceUnavailable"/>
- /// <seealso cref="ISsdpDeviceLocator.StartListeningForNotifications"/>
- /// <seealso cref="ISsdpDeviceLocator.StopListeningForNotifications"/>
- public string NotificationFilter
- {
- get;
- set;
- }
-
- /// <summary>
- /// Disposes this object and all internal resources. Stops listening for all network messages.
- /// </summary>
- /// <param name="disposing">True if managed resources should be disposed, or false is only unmanaged resources should be cleaned up.</param>
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- DisposeBroadcastTimer();
-
- var commsServer = _CommunicationsServer;
- _CommunicationsServer = null;
- if (commsServer is not null)
- {
- commsServer.ResponseReceived -= CommsServer_ResponseReceived;
- commsServer.RequestReceived -= CommsServer_RequestReceived;
- }
- }
- }
-
- private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress IPAddress)
- {
- bool isNewDevice = false;
- lock (_Devices)
- {
- var existingDevice = FindExistingDeviceNotification(_Devices, device.NotificationType, device.Usn);
- if (existingDevice is null)
- {
- _Devices.Add(device);
- isNewDevice = true;
- }
- else
- {
- _Devices.Remove(existingDevice);
- _Devices.Add(device);
- }
- }
-
- DeviceFound(device, isNewDevice, IPAddress);
- }
-
- private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
- {
- if (!NotificationTypeMatchesFilter(device))
- {
- return;
- }
-
- OnDeviceAvailable(device, isNewDevice, IPAddress);
- }
-
- private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device)
- {
- return String.IsNullOrEmpty(this.NotificationFilter)
- || this.NotificationFilter == SsdpConstants.SsdpDiscoverAllSTHeader
- || device.NotificationType == this.NotificationFilter;
- }
-
- private Task BroadcastDiscoverMessage(string serviceType, TimeSpan mxValue, CancellationToken cancellationToken)
- {
- const string header = "M-SEARCH * HTTP/1.1";
-
- var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort);
- values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
- values["MAN"] = "\"ssdp:discover\"";
-
- // Search target
- values["ST"] = "ssdp:all";
-
- // Seconds to delay response
- values["MX"] = "3";
-
- var message = BuildMessage(header, values);
-
- return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
- }
-
- private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress IPAddress)
- {
- if (!message.IsSuccessStatusCode)
- {
- return;
- }
-
- var location = GetFirstHeaderUriValue("Location", message);
- if (location is not null)
- {
- var device = new DiscoveredSsdpDevice()
- {
- DescriptionLocation = location,
- Usn = GetFirstHeaderStringValue("USN", message),
- NotificationType = GetFirstHeaderStringValue("ST", message),
- CacheLifetime = CacheAgeFromHeader(message.Headers.CacheControl),
- AsAt = DateTimeOffset.Now,
- ResponseHeaders = message.Headers
- };
-
- AddOrUpdateDiscoveredDevice(device, IPAddress);
- }
- }
-
- private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress)
- {
- if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
- {
- return;
- }
-
- var notificationType = GetFirstHeaderStringValue("NTS", message);
- if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
- {
- ProcessAliveNotification(message, IPAddress);
- }
- else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
- {
- ProcessByeByeNotification(message);
- }
- }
-
- private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IPAddress)
- {
- var location = GetFirstHeaderUriValue("Location", message);
- if (location is not null)
- {
- var device = new DiscoveredSsdpDevice()
- {
- DescriptionLocation = location,
- Usn = GetFirstHeaderStringValue("USN", message),
- NotificationType = GetFirstHeaderStringValue("NT", message),
- CacheLifetime = CacheAgeFromHeader(message.Headers.CacheControl),
- AsAt = DateTimeOffset.Now,
- ResponseHeaders = message.Headers
- };
-
- AddOrUpdateDiscoveredDevice(device, IPAddress);
- }
- }
-
- private void ProcessByeByeNotification(HttpRequestMessage message)
- {
- var notficationType = GetFirstHeaderStringValue("NT", message);
- if (!string.IsNullOrEmpty(notficationType))
- {
- var usn = GetFirstHeaderStringValue("USN", message);
-
- if (!DeviceDied(usn, false))
- {
- var deadDevice = new DiscoveredSsdpDevice()
- {
- AsAt = DateTime.UtcNow,
- CacheLifetime = TimeSpan.Zero,
- DescriptionLocation = null,
- NotificationType = GetFirstHeaderStringValue("NT", message),
- Usn = usn,
- ResponseHeaders = message.Headers
- };
-
- if (NotificationTypeMatchesFilter(deadDevice))
- {
- OnDeviceUnavailable(deadDevice, false);
- }
- }
- }
- }
-
- private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message)
- {
- string retVal = null;
- if (message.Headers.Contains(headerName))
- {
- message.Headers.TryGetValues(headerName, out var values);
- if (values is not null)
- {
- retVal = values.FirstOrDefault();
- }
- }
-
- return retVal;
- }
-
- private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message)
- {
- string retVal = null;
- if (message.Headers.Contains(headerName))
- {
- message.Headers.TryGetValues(headerName, out var values);
- if (values is not null)
- {
- retVal = values.FirstOrDefault();
- }
- }
-
- return retVal;
- }
-
- private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request)
- {
- string value = null;
- if (request.Headers.Contains(headerName))
- {
- request.Headers.TryGetValues(headerName, out var values);
- if (values is not null)
- {
- value = values.FirstOrDefault();
- }
- }
-
- Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var retVal);
- return retVal;
- }
-
- private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response)
- {
- string value = null;
- if (response.Headers.Contains(headerName))
- {
- response.Headers.TryGetValues(headerName, out var values);
- if (values is not null)
- {
- value = values.FirstOrDefault();
- }
- }
-
- Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var retVal);
- return retVal;
- }
-
- private TimeSpan CacheAgeFromHeader(System.Net.Http.Headers.CacheControlHeaderValue headerValue)
- {
- if (headerValue is null)
- {
- return TimeSpan.Zero;
- }
-
- return headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero;
- }
-
- private void RemoveExpiredDevicesFromCache()
- {
- DiscoveredSsdpDevice[] expiredDevices = null;
- lock (_Devices)
- {
- expiredDevices = (from device in _Devices where device.IsExpired() select device).ToArray();
-
- foreach (var device in expiredDevices)
- {
- if (IsDisposed)
- {
- return;
- }
-
- _Devices.Remove(device);
- }
- }
-
- // Don't do this inside lock because DeviceDied raises an event
- // which means public code may execute during lock and cause
- // problems.
- foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct())
- {
- if (IsDisposed)
- {
- return;
- }
-
- DeviceDied(expiredUsn, true);
- }
- }
-
- private bool DeviceDied(string deviceUsn, bool expired)
- {
- List<DiscoveredSsdpDevice> existingDevices = null;
- lock (_Devices)
- {
- existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn);
- foreach (var existingDevice in existingDevices)
- {
- if (IsDisposed)
- {
- return true;
- }
-
- _Devices.Remove(existingDevice);
- }
- }
-
- if (existingDevices is not null && existingDevices.Count > 0)
- {
- foreach (var removedDevice in existingDevices)
- {
- if (NotificationTypeMatchesFilter(removedDevice))
- {
- OnDeviceUnavailable(removedDevice, expired);
- }
- }
-
- return true;
- }
-
- return false;
- }
-
- private TimeSpan SearchTimeToMXValue(TimeSpan searchWaitTime)
- {
- if (searchWaitTime.TotalSeconds < 2 || searchWaitTime == TimeSpan.Zero)
- {
- return OneSecond;
- }
-
- return searchWaitTime.Subtract(OneSecond);
- }
-
- private DiscoveredSsdpDevice FindExistingDeviceNotification(IEnumerable<DiscoveredSsdpDevice> devices, string notificationType, string usn)
- {
- foreach (var d in devices)
- {
- if (d.NotificationType == notificationType && d.Usn == usn)
- {
- return d;
- }
- }
-
- return null;
- }
-
- private List<DiscoveredSsdpDevice> FindExistingDeviceNotifications(IList<DiscoveredSsdpDevice> devices, string usn)
- {
- var list = new List<DiscoveredSsdpDevice>();
-
- foreach (var d in devices)
- {
- if (d.Usn == usn)
- {
- list.Add(d);
- }
- }
-
- return list;
- }
-
- private void CommsServer_ResponseReceived(object sender, ResponseReceivedEventArgs e)
- {
- ProcessSearchResponseMessage(e.Message, e.LocalIPAddress);
- }
-
- private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e)
- {
- ProcessNotificationMessage(e.Message, e.ReceivedFrom.Address);
- }
- }
-}
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
deleted file mode 100644
index 65ae658a4..000000000
--- a/RSSDP/SsdpDevicePublisher.cs
+++ /dev/null
@@ -1,629 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Rssdp.Infrastructure
-{
- /// <summary>
- /// Provides the platform independent logic for publishing SSDP devices (notifications and search responses).
- /// </summary>
- public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
- {
- private ISsdpCommunicationsServer _CommsServer;
- private string _OSName;
- private string _OSVersion;
- private bool _sendOnlyMatchedHost;
-
- private bool _SupportPnpRootDevice;
-
- private IList<SsdpRootDevice> _Devices;
- private IReadOnlyList<SsdpRootDevice> _ReadOnlyDevices;
-
- private Timer _RebroadcastAliveNotificationsTimer;
-
- private IDictionary<string, SearchRequest> _RecentSearchRequests;
-
- private Random _Random;
-
- /// <summary>
- /// Default constructor.
- /// </summary>
- public SsdpDevicePublisher(
- ISsdpCommunicationsServer communicationsServer,
- string osName,
- string osVersion,
- bool sendOnlyMatchedHost)
- {
- ArgumentNullException.ThrowIfNull(communicationsServer);
- ArgumentNullException.ThrowIfNullOrEmpty(osName);
- ArgumentNullException.ThrowIfNullOrEmpty(osVersion);
-
- _SupportPnpRootDevice = true;
- _Devices = new List<SsdpRootDevice>();
- _ReadOnlyDevices = new ReadOnlyCollection<SsdpRootDevice>(_Devices);
- _RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
- _Random = new Random();
-
- _CommsServer = communicationsServer;
- _CommsServer.RequestReceived += CommsServer_RequestReceived;
- _OSName = osName;
- _OSVersion = osVersion;
- _sendOnlyMatchedHost = sendOnlyMatchedHost;
-
- _CommsServer.BeginListeningForMulticast();
-
- // Send alive notification once on creation
- SendAllAliveNotifications(null);
- }
-
- public void StartSendingAliveNotifications(TimeSpan interval)
- {
- _RebroadcastAliveNotificationsTimer = new Timer(SendAllAliveNotifications, null, TimeSpan.FromSeconds(5), interval);
- }
-
- /// <summary>
- /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients.
- /// </summary>
- /// <remarks>
- /// <para>Adding a device causes "alive" notification messages to be sent immediately, or very soon after. Ensure your device/description service is running before adding the device object here.</para>
- /// <para>Devices added here with a non-zero cache life time will also have notifications broadcast periodically.</para>
- /// <para>This method ignores duplicate device adds (if the same device instance is added multiple times, the second and subsequent add calls do nothing).</para>
- /// </remarks>
- /// <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>
- public void AddDevice(SsdpRootDevice device)
- {
- if (device is null)
- {
- throw new ArgumentNullException(nameof(device));
- }
-
- ThrowIfDisposed();
-
- bool wasAdded = false;
- lock (_Devices)
- {
- if (!_Devices.Contains(device))
- {
- _Devices.Add(device);
- wasAdded = true;
- }
- }
-
- if (wasAdded)
- {
- WriteTrace("Device Added", device);
-
- SendAliveNotifications(device, true, CancellationToken.None);
- }
- }
-
- /// <summary>
- /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable.
- /// </summary>
- /// <remarks>
- /// <para>Removing a device causes "byebye" notification messages to be sent immediately, advising clients of the device/service becoming unavailable. We recommend removing the device from the published list before shutting down the actual device/service, if possible.</para>
- /// <para>This method does nothing if the device was not found in the collection.</para>
- /// </remarks>
- /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param>
- /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
- public async Task RemoveDevice(SsdpRootDevice device)
- {
- if (device is null)
- {
- throw new ArgumentNullException(nameof(device));
- }
-
- bool wasRemoved = false;
- lock (_Devices)
- {
- if (_Devices.Contains(device))
- {
- _Devices.Remove(device);
- wasRemoved = true;
- }
- }
-
- if (wasRemoved)
- {
- WriteTrace("Device Removed", device);
-
- await SendByeByeNotifications(device, true, CancellationToken.None).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Returns a read only list of devices being published by this instance.
- /// </summary>
- public IEnumerable<SsdpRootDevice> Devices
- {
- get
- {
- return _ReadOnlyDevices;
- }
- }
-
- /// <summary>
- /// If true (default) treats root devices as both upnp:rootdevice and pnp:rootdevice types.
- /// </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 notification broadcasts and and search responses, which is correct according to the UPnP/SSDP spec.</para>
- /// </remarks>
- public bool SupportPnpRootDevice
- {
- get { return _SupportPnpRootDevice; }
-
- set
- {
- _SupportPnpRootDevice = value;
- }
- }
-
- /// <summary>
- /// Stops listening for requests, stops sending periodic broadcasts, disposes all internal resources.
- /// </summary>
- /// <param name="disposing"></param>
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- DisposeRebroadcastTimer();
-
- var commsServer = _CommsServer;
- if (commsServer is not null)
- {
- commsServer.RequestReceived -= this.CommsServer_RequestReceived;
- }
-
- var tasks = Devices.ToList().Select(RemoveDevice).ToArray();
- Task.WaitAll(tasks);
-
- _CommsServer = null;
- if (commsServer is not null)
- {
- if (!commsServer.IsShared)
- {
- commsServer.Dispose();
- }
- }
-
- _RecentSearchRequests = null;
- }
- }
-
- private void ProcessSearchRequest(
- string mx,
- string searchTarget,
- IPEndPoint remoteEndPoint,
- IPAddress receivedOnlocalIPAddress,
- CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(searchTarget))
- {
- WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
- return;
- }
-
- // WriteTrace(String.Format("Search Request Received From {0}, Target = {1}", remoteEndPoint.ToString(), searchTarget));
-
- if (IsDuplicateSearchRequest(searchTarget, remoteEndPoint))
- {
- // WriteTrace("Search Request is Duplicate, ignoring.");
- return;
- }
-
- // Wait on random interval up to MX, as per SSDP spec.
- // Also, as per UPnP 1.1/SSDP spec ignore missing/bank MX header. If over 120, assume random value between 0 and 120.
- // Using 16 as minimum as that's often the minimum system clock frequency anyway.
- if (String.IsNullOrEmpty(mx))
- {
- // Windows Explorer is poorly behaved and doesn't supply an MX header value.
- // if (this.SupportPnpRootDevice)
- mx = "1";
- // else
- // return;
- }
-
- if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0)
- {
- return;
- }
-
- if (maxWaitInterval > 120)
- {
- maxWaitInterval = _Random.Next(0, 120);
- }
-
- // Do not block synchronously as that may tie up a threadpool thread for several seconds.
- Task.Delay(_Random.Next(16, maxWaitInterval * 1000)).ContinueWith((parentTask) =>
- {
- // Copying devices to local array here to avoid threading issues/enumerator exceptions.
- IEnumerable<SsdpDevice> devices = null;
- lock (_Devices)
- {
- if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)
- {
- devices = GetAllDevicesAsFlatEnumerable().ToArray();
- }
- else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0))
- {
- devices = _Devices.ToArray();
- }
- else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase))
- {
- devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray();
- }
- else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase))
- {
- devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
- }
- }
-
- if (devices is not null)
- {
- // WriteTrace(String.Format("Sending {0} search responses", deviceList.Count));
-
- foreach (var device in devices)
- {
- var root = device.ToRootDevice();
-
- if (!_sendOnlyMatchedHost || root.Address.Equals(receivedOnlocalIPAddress))
- {
- SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIPAddress, cancellationToken);
- }
- }
- }
- });
- }
-
- private IEnumerable<SsdpDevice> GetAllDevicesAsFlatEnumerable()
- {
- return _Devices.Union(_Devices.SelectManyRecursive<SsdpDevice>((d) => d.Devices));
- }
-
- private void SendDeviceSearchResponses(
- SsdpDevice device,
- IPEndPoint endPoint,
- IPAddress receivedOnlocalIPAddress,
- CancellationToken cancellationToken)
- {
- bool isRootDevice = (device as SsdpRootDevice) is not null;
- if (isRootDevice)
- {
- SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
- if (SupportPnpRootDevice)
- {
- SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
- }
- }
-
- SendSearchResponse(device.Udn, device, device.Udn, endPoint, receivedOnlocalIPAddress, cancellationToken);
-
- SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint, receivedOnlocalIPAddress, cancellationToken);
- }
-
- private string GetUsn(string udn, string fullDeviceType)
- {
- return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
- }
-
- private async void SendSearchResponse(
- string searchTarget,
- SsdpDevice device,
- string uniqueServiceName,
- IPEndPoint endPoint,
- IPAddress receivedOnlocalIPAddress,
- CancellationToken cancellationToken)
- {
- const string header = "HTTP/1.1 200 OK";
-
- var rootDevice = device.ToRootDevice();
- var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- ["EXT"] = "",
- ["DATE"] = DateTime.UtcNow.ToString("r"),
- ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
- ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
- ["ST"] = searchTarget,
- ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
- ["USN"] = uniqueServiceName,
- ["LOCATION"] = rootDevice.Location.ToString()
- };
-
- var message = BuildMessage(header, values);
-
- try
- {
- await _CommsServer.SendMessage(
- Encoding.UTF8.GetBytes(message),
- endPoint,
- receivedOnlocalIPAddress,
- cancellationToken)
- .ConfigureAwait(false);
- }
- catch (Exception)
- {
- }
-
- // WriteTrace(String.Format("Sent search response to " + endPoint.ToString()), device);
- }
-
- private bool IsDuplicateSearchRequest(string searchTarget, IPEndPoint endPoint)
- {
- var isDuplicateRequest = false;
-
- var newRequest = new SearchRequest() { EndPoint = endPoint, SearchTarget = searchTarget, Received = DateTime.UtcNow };
- lock (_RecentSearchRequests)
- {
- if (_RecentSearchRequests.ContainsKey(newRequest.Key))
- {
- var lastRequest = _RecentSearchRequests[newRequest.Key];
- if (lastRequest.IsOld())
- {
- _RecentSearchRequests[newRequest.Key] = newRequest;
- }
- else
- {
- isDuplicateRequest = true;
- }
- }
- else
- {
- _RecentSearchRequests.Add(newRequest.Key, newRequest);
- if (_RecentSearchRequests.Count > 10)
- {
- CleanUpRecentSearchRequestsAsync();
- }
- }
- }
-
- return isDuplicateRequest;
- }
-
- private void CleanUpRecentSearchRequestsAsync()
- {
- lock (_RecentSearchRequests)
- {
- foreach (var requestKey in (from r in _RecentSearchRequests where r.Value.IsOld() select r.Key).ToArray())
- {
- _RecentSearchRequests.Remove(requestKey);
- }
- }
- }
-
- private void SendAllAliveNotifications(object state)
- {
- try
- {
- if (IsDisposed)
- {
- return;
- }
-
- // WriteTrace("Begin Sending Alive Notifications For All Devices");
-
- SsdpRootDevice[] devices;
- lock (_Devices)
- {
- devices = _Devices.ToArray();
- }
-
- foreach (var device in devices)
- {
- if (IsDisposed)
- {
- return;
- }
-
- SendAliveNotifications(device, true, CancellationToken.None);
- }
-
- // WriteTrace("Completed Sending Alive Notifications For All Devices");
- }
- catch (ObjectDisposedException ex)
- {
- WriteTrace("Publisher stopped, exception " + ex.Message);
- Dispose();
- }
- }
-
- private void SendAliveNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken)
- {
- if (isRoot)
- {
- SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken);
- if (SupportPnpRootDevice)
- {
- SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken);
- }
- }
-
- SendAliveNotification(device, device.Udn, device.Udn, cancellationToken);
- SendAliveNotification(device, device.FullDeviceType, GetUsn(device.Udn, device.FullDeviceType), cancellationToken);
-
- foreach (var childDevice in device.Devices)
- {
- SendAliveNotifications(childDevice, false, cancellationToken);
- }
- }
-
- private void SendAliveNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken)
- {
- var rootDevice = device.ToRootDevice();
-
- const string header = "NOTIFY * HTTP/1.1";
-
- var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- // If needed later for non-server devices, these headers will need to be dynamic
- ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
- ["DATE"] = DateTime.UtcNow.ToString("r"),
- ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
- ["LOCATION"] = rootDevice.Location.ToString(),
- ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
- ["NTS"] = "ssdp:alive",
- ["NT"] = notificationType,
- ["USN"] = uniqueServiceName
- };
-
- var message = BuildMessage(header, values);
-
- _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken);
-
- // WriteTrace(String.Format("Sent alive notification"), device);
- }
-
- private Task SendByeByeNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken)
- {
- var tasks = new List<Task>();
- if (isRoot)
- {
- tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken));
- if (SupportPnpRootDevice)
- {
- tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken));
- }
- }
-
- tasks.Add(SendByeByeNotification(device, device.Udn, device.Udn, cancellationToken));
- tasks.Add(SendByeByeNotification(device, String.Format(CultureInfo.InvariantCulture, "urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken));
-
- foreach (var childDevice in device.Devices)
- {
- tasks.Add(SendByeByeNotifications(childDevice, false, cancellationToken));
- }
-
- return Task.WhenAll(tasks);
- }
-
- private Task SendByeByeNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken)
- {
- const string header = "NOTIFY * HTTP/1.1";
-
- var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- // If needed later for non-server devices, these headers will need to be dynamic
- ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
- ["DATE"] = DateTime.UtcNow.ToString("r"),
- ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
- ["NTS"] = "ssdp:byebye",
- ["NT"] = notificationType,
- ["USN"] = uniqueServiceName
- };
-
- var message = BuildMessage(header, values);
-
- var sendCount = IsDisposed ? 1 : 3;
- WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
- return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
- }
-
- private void DisposeRebroadcastTimer()
- {
- var timer = _RebroadcastAliveNotificationsTimer;
- _RebroadcastAliveNotificationsTimer = null;
- if (timer is not null)
- {
- timer.Dispose();
- }
- }
-
- private TimeSpan GetMinimumNonZeroCacheLifetime()
- {
- var nonzeroCacheLifetimesQuery = (
- from device
- in _Devices
- where device.CacheLifetime != TimeSpan.Zero
- select device.CacheLifetime).ToList();
-
- if (nonzeroCacheLifetimesQuery.Any())
- {
- return nonzeroCacheLifetimesQuery.Min();
- }
-
- return TimeSpan.Zero;
- }
-
- private string GetFirstHeaderValue(System.Net.Http.Headers.HttpRequestHeaders httpRequestHeaders, string headerName)
- {
- string retVal = null;
- if (httpRequestHeaders.TryGetValues(headerName, out var values) && values is not null)
- {
- retVal = values.FirstOrDefault();
- }
-
- return retVal;
- }
-
- public Action<string> LogFunction { get; set; }
-
- private void WriteTrace(string text)
- {
- if (LogFunction is not null)
- {
- LogFunction(text);
- }
- // System.Diagnostics.Debug.WriteLine(text, "SSDP Publisher");
- }
-
- private void WriteTrace(string text, SsdpDevice device)
- {
- var rootDevice = device as SsdpRootDevice;
- if (rootDevice is not null)
- {
- WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid + " - " + rootDevice.Location);
- }
- else
- {
- WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid);
- }
- }
-
- private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e)
- {
- if (this.IsDisposed)
- {
- return;
- }
-
- if (string.Equals(e.Message.Method.Method, SsdpConstants.MSearchMethod, StringComparison.OrdinalIgnoreCase))
- {
- // According to SSDP/UPnP spec, ignore message if missing these headers.
- // Edit: But some devices do it anyway
- // if (!e.Message.Headers.Contains("MX"))
- // WriteTrace("Ignoring search request - missing MX header.");
- // else if (!e.Message.Headers.Contains("MAN"))
- // WriteTrace("Ignoring search request - missing MAN header.");
- // else
- ProcessSearchRequest(GetFirstHeaderValue(e.Message.Headers, "MX"), GetFirstHeaderValue(e.Message.Headers, "ST"), e.ReceivedFrom, e.LocalIPAddress, CancellationToken.None);
- }
- }
-
- private class SearchRequest
- {
- public IPEndPoint EndPoint { get; set; }
-
- public DateTime Received { get; set; }
-
- public string SearchTarget { get; set; }
-
- public string Key
- {
- get { return this.SearchTarget + ":" + this.EndPoint; }
- }
-
- public bool IsOld()
- {
- return DateTime.UtcNow.Subtract(this.Received).TotalMilliseconds > 500;
- }
- }
- }
-}
diff --git a/RSSDP/SsdpEmbeddedDevice.cs b/RSSDP/SsdpEmbeddedDevice.cs
deleted file mode 100644
index f1a598111..000000000
--- a/RSSDP/SsdpEmbeddedDevice.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-namespace Rssdp
-{
- /// <summary>
- /// Represents a device that is a descendant of a <see cref="SsdpRootDevice"/> instance.
- /// </summary>
- public class SsdpEmbeddedDevice : SsdpDevice
- {
- private SsdpRootDevice _RootDevice;
-
- /// <summary>
- /// Default constructor.
- /// </summary>
- public SsdpEmbeddedDevice()
- {
- }
-
- /// <summary>
- /// Returns the <see cref="SsdpRootDevice"/> that is this device's first ancestor. If this device is itself an <see cref="SsdpRootDevice"/>, then returns a reference to itself.
- /// </summary>
- public SsdpRootDevice RootDevice
- {
- get
- {
- return _RootDevice;
- }
-
- internal set
- {
- _RootDevice = value;
- lock (this.Devices)
- {
- foreach (var embeddedDevice in this.Devices)
- {
- ((SsdpEmbeddedDevice)embeddedDevice).RootDevice = _RootDevice;
- }
- }
- }
- }
- }
-}
diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs
deleted file mode 100644
index 5ecb1f86f..000000000
--- a/RSSDP/SsdpRootDevice.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.Net;
-
-namespace Rssdp
-{
- /// <summary>
- /// Represents a 'root' device, a device that has no parent. Used for publishing devices and for the root device in a tree of discovered devices.
- /// </summary>
- /// <remarks>
- /// <para>Child (embedded) devices are represented by the <see cref="SsdpDevice"/> in the <see cref="SsdpDevice.Devices"/> property.</para>
- /// <para>Root devices contain some information that applies to the whole device tree and is therefore not present on child devices, such as <see cref="CacheLifetime"/> and <see cref="Location"/>.</para>
- /// </remarks>
- public class SsdpRootDevice : SsdpDevice
- {
- private Uri _UrlBase;
-
- /// <summary>
- /// Default constructor.
- /// </summary>
- public SsdpRootDevice() : base()
- {
- }
-
- /// <summary>
- /// 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>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>
- public TimeSpan CacheLifetime
- {
- get; set;
- }
-
- /// <summary>
- /// Gets or sets the URL used to retrieve the description document for this device/tree. Required.
- /// </summary>
- public Uri Location { get; set; }
-
- /// <summary>
- /// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required.
- /// </summary>
- public IPAddress Address { get; set; }
-
- /// <summary>
- /// Gets or sets the prefix length used to check if the received message from same interface with this device/tree. Required.
- /// </summary>
- public byte PrefixLength { get; set; }
-
- /// <summary>
- /// 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>
- /// </remarks>
- public Uri UrlBase
- {
- get
- {
- return _UrlBase ?? this.Location;
- }
-
- set
- {
- _UrlBase = value;
- }
- }
- }
-}
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
index aec1d4d10..af460fedc 100644
--- a/debian/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -24,6 +24,9 @@ JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web"
# ffmpeg binary paths, overriding the system values
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
+# Disable glibc dynamic heap adjustment
+MALLOC_TRIM_THRESHOLD_=131072
+
# [OPTIONAL] run Jellyfin as a headless service
#JELLYFIN_SERVICE_OPT="--service"
diff --git a/debian/control b/debian/control
index 0b9dd570e..5e0460de9 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-7.0,
+ dotnet-sdk-8.0,
libc6-dev,
libcurl4-openssl-dev,
libfontconfig1-dev,
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 6c2086bee..7c9bbf39e 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.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.amd64 b/deployment/Dockerfile.debian.amd64
index 1e1f6e54e..d344c5964 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index bbed2c534..8a5411f05 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index 79373519c..e95ba1696 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64
index 3a6ad95e8..1749ca563 100644
--- a/deployment/Dockerfile.docker.amd64
+++ b/deployment/Dockerfile.docker.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64
index ca7239304..bbddb61e4 100644
--- a/deployment/Dockerfile.docker.arm64
+++ b/deployment/Dockerfile.docker.arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf
index 26cce1958..3de1d6887 100644
--- a/deployment/Dockerfile.docker.armhf
+++ b/deployment/Dockerfile.docker.armhf
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 16002f790..66ead37d7 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.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.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 39169bd2a..386f7cefe 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
index 636a34544..56c877333 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
index ba8ce82f0..c9692c440 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
index d771e9991..230461556 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.musl-linux-arm64 b/deployment/Dockerfile.linux.musl-linux-arm64
index 846561181..240d09186 100644
--- a/deployment/Dockerfile.linux.musl-linux-arm64
+++ b/deployment/Dockerfile.linux.musl-linux-arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.macos.amd64 b/deployment/Dockerfile.macos.amd64
index 7ebf35442..1b054dfc4 100644
--- a/deployment/Dockerfile.macos.amd64
+++ b/deployment/Dockerfile.macos.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.macos.arm64 b/deployment/Dockerfile.macos.arm64
index 5041ff967..07e18da55 100644
--- a/deployment/Dockerfile.macos.arm64
+++ b/deployment/Dockerfile.macos.arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 822b66ee6..36135f7a6 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 2befb4b55..84fa2028e 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.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 497a7a3dd..ca3aa3508 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.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 fae7b209f..e52b7fba3 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.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 805c63f8c..08587aa7e 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim
+FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
index d92953ad1..7e968192b 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-7.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-7.0,/d' debian/control
+ sed -i '/dotnet-sdk-8.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 618a121b6..7b7b603d6 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-7.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-7.0,/d' debian/control
+ sed -i '/dotnet-sdk-8.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 d1631d022..3d894ba20 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-7.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-7.0,/d' debian/control
+ sed -i '/dotnet-sdk-8.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 4254103fa..5f25cb610 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-7.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-7.0,/d' debian/control
+ sed -i '/dotnet-sdk-8.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 42f111a01..334ced997 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-7.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-7.0,/d' debian/control
+ sed -i '/dotnet-sdk-8.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 357d63626..77e33c307 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-7.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-7.0,/d' debian/control
+ sed -i '/dotnet-sdk-8.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/unraid/docker-templates/jellyfin.xml b/deployment/unraid/docker-templates/jellyfin.xml
index 57b4cc5ae..69742066e 100644
--- a/deployment/unraid/docker-templates/jellyfin.xml
+++ b/deployment/unraid/docker-templates/jellyfin.xml
@@ -21,7 +21,7 @@
<Registry>https://hub.docker.com/r/jellyfin/jellyfin/</Registry>
<GitHub>https://github.com/jellyfin/jellyfin/></GitHub>
<Repository>jellyfin/jellyfin</Repository>
- <Project>https://jellyfin.media/</Project>
+ <Project>https://jellyfin.org/</Project>
<BindTime>true</BindTime>
<Privileged>false</Privileged>
<Networking>
diff --git a/fedora/jellyfin.env b/fedora/jellyfin.env
index 1f79fac4f..cee8f6854 100644
--- a/fedora/jellyfin.env
+++ b/fedora/jellyfin.env
@@ -23,6 +23,12 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
# web client path, installed by the jellyfin-web package
# JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web"
+# In-App service control
+JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
+
+# Disable glibc dynamic heap adjustment
+MALLOC_TRIM_THRESHOLD_=131072
+
# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index e78368906..fb9fb2f7d 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -26,7 +26,7 @@ BuildRequires: systemd
BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel
# Requirements not packaged in RHEL 7 main repos, added via Makefile
# https://packages.microsoft.com/rhel/7/prod/
-BuildRequires: dotnet-runtime-7.0, dotnet-sdk-7.0
+BuildRequires: dotnet-runtime-8.0, dotnet-sdk-8.0
Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
# Temporary (hopefully?) fix for https://github.com/jellyfin/jellyfin/issues/7471
@@ -73,7 +73,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
%install
# Jellyfin files
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
-%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
+%{__cp} -r Jellyfin.Server/bin/Release/net8.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
index 1e3f8a048..73aae3f3d 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
index 03b296494..1571b5ab0 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
@@ -6,6 +6,7 @@ using Emby.Server.Implementations.Library;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Configuration;
using Moq;
using SharpFuzz;
@@ -54,8 +55,16 @@ namespace Emby.Server.Implementations.Fuzz
appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
.Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal));
+ var configSection = new Mock<IConfigurationSection>();
+ configSection.SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)])
+ .Returns("0");
+ var config = new Mock<IConfiguration>();
+ config.Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)))
+ .Returns(configSection.Object);
+
IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
fixture.Inject(appHost);
+ fixture.Inject(config);
return fixture.Create<SqliteItemRepository>();
}
}
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
index 37e6bdb76..80a5cd7c1 100755
--- a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
+++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
@@ -8,4 +8,4 @@ cp bin/Emby.Server.Implementations.dll .
dotnet build
mkdir -p Findings
-AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net6.0/Emby.Server.Implementations.Fuzz.dll "$1"
+AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Emby.Server.Implementations.Fuzz "$1"
diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
index 20bc4c724..faac7d976 100644
--- a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
+++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
@@ -2,12 +2,12 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
- <Reference Include="Jellyfin.Server">
- <HintPath>jellyfin.dll</HintPath>
+ <Reference Include="Jellyfin.Api">
+ <HintPath>Jellyfin.Api.dll</HintPath>
</Reference>
</ItemGroup>
diff --git a/fuzz/Jellyfin.Server.Fuzz/Program.cs b/fuzz/Jellyfin.Api.Fuzz/Program.cs
index e47286c13..6713322ac 100644
--- a/fuzz/Jellyfin.Server.Fuzz/Program.cs
+++ b/fuzz/Jellyfin.Api.Fuzz/Program.cs
@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
-using Jellyfin.Server.Middleware;
+using Jellyfin.Api.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using SharpFuzz;
-namespace Emby.Server.Implementations.Fuzz
+namespace Jellyfin.Api.Fuzz
{
public static class Program
{
diff --git a/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt b/fuzz/Jellyfin.Api.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
index 73f356b93..73f356b93 100644
--- a/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
+++ b/fuzz/Jellyfin.Api.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
diff --git a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh
new file mode 100755
index 000000000..96b0192cf
--- /dev/null
+++ b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+set -e
+
+dotnet build -c Release ../../Jellyfin.Api/Jellyfin.Api.csproj --output bin
+sharpfuzz bin/Jellyfin.Api.dll
+cp bin/Jellyfin.Api.dll .
+
+dotnet build
+mkdir -p Findings
+AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Jellyfin.Api.Fuzz "$1"
diff --git a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh
deleted file mode 100755
index 303eb2135..000000000
--- a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-
-set -e
-
-dotnet build -c Release ../../Jellyfin.Server/Jellyfin.Server.csproj --output bin
-sharpfuzz bin/jellyfin.dll
-cp bin/jellyfin.dll .
-
-dotnet build
-mkdir -p Findings
-AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net6.0/Jellyfin.Server.Fuzz.dll "$1"
diff --git a/fuzz/README.md b/fuzz/README.md
new file mode 100644
index 000000000..25ba7d05c
--- /dev/null
+++ b/fuzz/README.md
@@ -0,0 +1,20 @@
+# Jellyfin fuzzing
+
+## Setup
+
+Install AFL++
+```sh
+git clone https://github.com/AFLplusplus/AFLplusplus
+cd AFLplusplus
+make all
+sudo make install
+```
+
+Install SharpFuzz.CommandLine global .NET tool
+```sh
+dotnet tool install --global SharpFuzz.CommandLine
+```
+
+## Running
+Run the `fuzz.sh` in the directory corresponding to the project you want to fuzz.
+The script takes a parameter of which fuzz case you want to run.
diff --git a/global.json b/global.json
index 24335d7a0..9db4b532c 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "7.0.0",
+ "version": "8.0.0",
"rollForward": "latestMinor"
}
}
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 870cf253f..10225e3af 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -140,6 +140,9 @@
<Rule Id="CA1812" Action="Info" />
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
<Rule Id="CA1822" Action="Info" />
+ <!-- TODO: Enable -->
+ <!-- CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array -->
+ <Rule Id="CA1861" Action="Info" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="Info" />
<!-- disable warning CA2253: Named placeholders should not be numeric values -->
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Directory.Build.props
index 4cff5927f..ac2726ed5 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/src/Directory.Build.props
@@ -1,16 +1,14 @@
-<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <GenerateDocumentationFile>true</GenerateDocumentationFile>
- </PropertyGroup>
+<Project>
+ <!-- Sets defaults for all projects -->
- <ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
- </ItemGroup>
+ <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@@ -20,8 +18,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
- <ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
- <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
- </ItemGroup>
</Project>
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index c465c4ad0..0590ded32 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -6,9 +6,11 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <!-- TODO: Remove once we update SkiaSharp > 2.88.5 -->
+ <NoWarn>NU1903</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -18,11 +20,11 @@
<ItemGroup>
<PackageReference Include="BlurHashSharp" />
<PackageReference Include="BlurHashSharp.SkiaSharp" />
+ <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
- <PackageReference Include="SkiaSharp.Svg" />
<PackageReference Include="SkiaSharp.HarfBuzz" />
- <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" />
+ <PackageReference Include="Svg.Skia" />
</ItemGroup>
<ItemGroup>
@@ -31,19 +33,4 @@
<ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="IDisposableAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
</Project>
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 03f90da8e..5721e2882 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -10,7 +10,7 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing;
using Microsoft.Extensions.Logging;
using SkiaSharp;
-using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
+using Svg.Skia;
namespace Jellyfin.Drawing.Skia;
@@ -23,6 +23,30 @@ public class SkiaEncoder : IImageEncoder
private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths;
+ private static readonly SKImageFilter _imageFilter;
+
+#pragma warning disable CA1810
+ static SkiaEncoder()
+#pragma warning restore CA1810
+ {
+ var kernel = new[]
+ {
+ 0, -.1f, 0,
+ -.1f, 1.4f, -.1f,
+ 0, -.1f, 0,
+ };
+
+ var kernelSize = new SKSizeI(3, 3);
+ var kernelOffset = new SKPointI(1, 1);
+ _imageFilter = SKImageFilter.CreateMatrixConvolution(
+ kernelSize,
+ kernel,
+ 1f,
+ 0f,
+ kernelOffset,
+ SKShaderTileMode.Clamp,
+ true);
+ }
/// <summary>
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
@@ -119,10 +143,16 @@ public class SkiaEncoder : IImageEncoder
var extension = Path.GetExtension(path.AsSpan());
if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
{
- var svg = new SKSvg();
+ using var svg = new SKSvg();
try
{
using var picture = svg.Load(path);
+ if (picture is null)
+ {
+ _logger.LogError("Unable to determine image dimensions for {FilePath}", path);
+ return default;
+ }
+
return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Height));
}
catch (FormatException skiaColorException)
@@ -285,10 +315,7 @@ public class SkiaEncoder : IImageEncoder
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{
- var needsFlip = origin == SKEncodedOrigin.LeftBottom
- || origin == SKEncodedOrigin.LeftTop
- || origin == SKEncodedOrigin.RightBottom
- || origin == SKEncodedOrigin.RightTop;
+ var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop;
var rotated = needsFlip
? new SKBitmap(bitmap.Height, bitmap.Width)
: new SKBitmap(bitmap.Width, bitmap.Height);
@@ -353,25 +380,7 @@ public class SkiaEncoder : IImageEncoder
IsDither = isDither
};
- var kernel = new float[9]
- {
- 0, -.1f, 0,
- -.1f, 1.4f, -.1f,
- 0, -.1f, 0,
- };
-
- var kernelSize = new SKSizeI(3, 3);
- var kernelOffset = new SKPointI(1, 1);
-
- paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
- kernelSize,
- kernel,
- 1f,
- 0f,
- kernelOffset,
- SKShaderTileMode.Clamp,
- true);
-
+ paint.ImageFilter = _imageFilter;
canvas.DrawBitmap(
source,
SKRect.Create(0, 0, source.Width, source.Height),
@@ -515,6 +524,81 @@ public class SkiaEncoder : IImageEncoder
splashBuilder.GenerateSplash(posters, backdrops, outputPath);
}
+ /// <inheritdoc />
+ public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+ {
+ var paths = options.InputPaths;
+ var tileWidth = options.Width;
+ var tileHeight = options.Height;
+
+ if (paths.Count < 1)
+ {
+ throw new ArgumentException("InputPaths cannot be empty.");
+ }
+ else if (paths.Count > tileWidth * tileHeight)
+ {
+ throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} grid.");
+ }
+
+ // If no height provided, use height of first image.
+ if (!imgHeight.HasValue)
+ {
+ using var firstImg = Decode(paths[0], false, null, out _);
+
+ if (firstImg is null)
+ {
+ throw new InvalidDataException("Could not decode image data.");
+ }
+
+ if (firstImg.Width != imgWidth)
+ {
+ throw new InvalidOperationException("Image width does not match provided width.");
+ }
+
+ imgHeight = firstImg.Height;
+ }
+
+ // Make horizontal strips using every provided image.
+ using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
+ using var canvas = new SKCanvas(tileGrid);
+
+ var imgIndex = 0;
+ for (var y = 0; y < tileHeight; y++)
+ {
+ for (var x = 0; x < tileWidth; x++)
+ {
+ if (imgIndex >= paths.Count)
+ {
+ break;
+ }
+
+ using var img = Decode(paths[imgIndex++], false, null, out _);
+
+ if (img is null)
+ {
+ throw new InvalidDataException("Could not decode image data.");
+ }
+
+ if (img.Width != imgWidth)
+ {
+ throw new InvalidOperationException("Image width does not match provided width.");
+ }
+
+ if (img.Height != imgHeight)
+ {
+ throw new InvalidOperationException("Image height does not match first image height.");
+ }
+
+ canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
+ }
+ }
+
+ using var outputStream = new SKFileWStream(options.OutputPath);
+ tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
+
+ return imgHeight.Value;
+ }
+
private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
{
try
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index 2a5e24a44..23c4c0a9a 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -21,19 +21,4 @@
<Compile Include="..\..\SharedVersion.cs" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="IDisposableAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
</Project>
diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs
index 171128bed..1495661c1 100644
--- a/src/Jellyfin.Drawing/NullImageEncoder.cs
+++ b/src/Jellyfin.Drawing/NullImageEncoder.cs
@@ -50,6 +50,12 @@ public class NullImageEncoder : IImageEncoder
}
/// <inheritdoc />
+ public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
public string GetImageBlurHash(int xComp, int yComp, string path)
{
throw new NotImplementedException();
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 36ae55ed2..c91f5d008 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -27,24 +27,8 @@
<Compile Include="../../SharedVersion.cs" />
</ItemGroup>
-
<ItemGroup>
<PackageReference Include="Diacritics" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="IDisposableAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
</Project>
diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
index 4d56ca615..9e6d4c3f8 100644
--- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs
+++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
using Jellyfin.Extensions.Json.Converters;
namespace Jellyfin.Extensions.Json
@@ -41,7 +42,8 @@ namespace Jellyfin.Extensions.Json
new JsonNullableStructConverterFactory(),
new JsonDateTimeConverter(),
new JsonStringConverter()
- }
+ },
+ TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new(_jsonSerializerOptions)
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index b792e7ec6..ee79802a1 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -1,25 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="IDisposableAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
index 479e6ffdc..720d987f1 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
@@ -11,8 +11,6 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfProbe;
/// </summary>
public static class FfProbeKeyframeExtractor
{
- private const string DefaultArguments = "-fflags +genpts -v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\"";
-
/// <summary>
/// Extracts the keyframes using the ffprobe executable at the specified path.
/// </summary>
@@ -26,7 +24,10 @@ public static class FfProbeKeyframeExtractor
StartInfo = new ProcessStartInfo
{
FileName = ffProbePath,
- Arguments = string.Format(CultureInfo.InvariantCulture, DefaultArguments, filePath),
+ Arguments = string.Format(
+ CultureInfo.InvariantCulture,
+ "-fflags +genpts -v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\"",
+ filePath),
CreateNoWindow = true,
UseShellExecute = false,
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 09b1f8faa..c79dcee3c 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -9,21 +9,6 @@
<PackageReference Include="NEbml" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="IDisposableAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
diff --git a/src/Jellyfin.Networking/ExternalPortForwarding.cs b/src/Jellyfin.Networking/ExternalPortForwarding.cs
new file mode 100644
index 000000000..df9e43ca9
--- /dev/null
+++ b/src/Jellyfin.Networking/ExternalPortForwarding.cs
@@ -0,0 +1,195 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Logging;
+using Mono.Nat;
+
+namespace Jellyfin.Networking;
+
+/// <summary>
+/// Server entrypoint handling external port forwarding.
+/// </summary>
+public sealed class ExternalPortForwarding : IServerEntryPoint
+{
+ private readonly IServerApplicationHost _appHost;
+ private readonly ILogger<ExternalPortForwarding> _logger;
+ private readonly IServerConfigurationManager _config;
+
+ private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
+
+ private Timer _timer;
+ private string _configIdentifier;
+
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="config">The configuration manager.</param>
+ public ExternalPortForwarding(
+ ILogger<ExternalPortForwarding> logger,
+ IServerApplicationHost appHost,
+ IServerConfigurationManager config)
+ {
+ _logger = logger;
+ _appHost = appHost;
+ _config = config;
+ }
+
+ private string GetConfigIdentifier()
+ {
+ const char Separator = '|';
+ var config = _config.GetNetworkConfiguration();
+
+ return new StringBuilder(32)
+ .Append(config.EnableUPnP).Append(Separator)
+ .Append(config.PublicHttpPort).Append(Separator)
+ .Append(config.PublicHttpsPort).Append(Separator)
+ .Append(_appHost.HttpPort).Append(Separator)
+ .Append(_appHost.HttpsPort).Append(Separator)
+ .Append(_appHost.ListenWithHttps).Append(Separator)
+ .Append(config.EnableRemoteAccess).Append(Separator)
+ .ToString();
+ }
+
+ private void OnConfigurationUpdated(object sender, EventArgs e)
+ {
+ var oldConfigIdentifier = _configIdentifier;
+ _configIdentifier = GetConfigIdentifier();
+
+ if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
+ {
+ Stop();
+ Start();
+ }
+ }
+
+ /// <inheritdoc />
+ public Task RunAsync()
+ {
+ Start();
+
+ _config.ConfigurationUpdated += OnConfigurationUpdated;
+
+ return Task.CompletedTask;
+ }
+
+ private void Start()
+ {
+ var config = _config.GetNetworkConfiguration();
+ if (!config.EnableUPnP || !config.EnableRemoteAccess)
+ {
+ return;
+ }
+
+ _logger.LogInformation("Starting NAT discovery");
+
+ NatUtility.DeviceFound += OnNatUtilityDeviceFound;
+ NatUtility.StartDiscovery();
+
+ _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
+ }
+
+ private void Stop()
+ {
+ _logger.LogInformation("Stopping NAT discovery");
+
+ NatUtility.StopDiscovery();
+ NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
+
+ _timer?.Dispose();
+ }
+
+ private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
+ {
+ try
+ {
+ await CreateRules(e.Device).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating port forwarding rules");
+ }
+ }
+
+ private Task CreateRules(INatDevice device)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ // On some systems the device discovered event seems to fire repeatedly
+ // This check will help ensure we're not trying to port map the same device over and over
+ if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
+ {
+ return Task.CompletedTask;
+ }
+
+ return Task.WhenAll(CreatePortMaps(device));
+ }
+
+ private IEnumerable<Task> CreatePortMaps(INatDevice device)
+ {
+ var config = _config.GetNetworkConfiguration();
+ yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
+
+ if (_appHost.ListenWithHttps)
+ {
+ yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
+ }
+ }
+
+ private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
+ {
+ _logger.LogDebug(
+ "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
+ privatePort,
+ publicPort,
+ device.DeviceEndpoint);
+
+ try
+ {
+ var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
+ await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(
+ ex,
+ "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
+ privatePort,
+ publicPort,
+ device.DeviceEndpoint);
+ }
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _config.ConfigurationUpdated -= OnConfigurationUpdated;
+
+ Stop();
+
+ _timer?.Dispose();
+ _timer = null;
+
+ _disposed = true;
+ }
+}
diff --git a/src/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs b/src/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs
new file mode 100644
index 000000000..7d86434b8
--- /dev/null
+++ b/src/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs
@@ -0,0 +1,119 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) .NET Foundation and Contributors
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+using System.IO;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Jellyfin.Networking.HappyEyeballs;
+
+/// <summary>
+/// Defines the <see cref="HttpClientExtension"/> class.
+///
+/// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 .
+/// </summary>
+public static class HttpClientExtension
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether the client should use IPv6.
+ /// </summary>
+ public static bool UseIPv6 { get; set; } = true;
+
+ /// <summary>
+ /// Implements the httpclient callback method.
+ /// </summary>
+ /// <param name="context">The <see cref="SocketsHttpConnectionContext"/> instance.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/> instance.</param>
+ /// <returns>The http steam.</returns>
+ public static async ValueTask<Stream> OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
+ {
+ if (!UseIPv6)
+ {
+ return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false);
+ }
+
+ using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token);
+
+ // GetAwaiter().GetResult() is used instead of .Result as this results in improved exception handling.
+ // The tasks have already been completed.
+ // See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details.
+ if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully)
+ {
+ await cancelIPv6.CancelAsync().ConfigureAwait(false);
+ return tryConnectAsyncIPv6.GetAwaiter().GetResult();
+ }
+
+ using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token);
+
+ if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6)
+ {
+ if (tryConnectAsyncIPv6.IsCompletedSuccessfully)
+ {
+ await cancelIPv4.CancelAsync().ConfigureAwait(false);
+ return tryConnectAsyncIPv6.GetAwaiter().GetResult();
+ }
+
+ return tryConnectAsyncIPv4.GetAwaiter().GetResult();
+ }
+ else
+ {
+ if (tryConnectAsyncIPv4.IsCompletedSuccessfully)
+ {
+ await cancelIPv6.CancelAsync().ConfigureAwait(false);
+ return tryConnectAsyncIPv4.GetAwaiter().GetResult();
+ }
+
+ return tryConnectAsyncIPv6.GetAwaiter().GetResult();
+ }
+ }
+
+ private static async Task<Stream> AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
+ {
+ // The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
+ var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
+ {
+ // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
+ NoDelay = true
+ };
+
+ try
+ {
+ await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
+ // The stream should take the ownership of the underlying socket,
+ // closing it when it's disposed.
+ return new NetworkStream(socket, ownsSocket: true);
+ }
+ catch
+ {
+ socket.Dispose();
+ throw;
+ }
+ }
+}
diff --git a/src/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Jellyfin.Networking/Jellyfin.Networking.csproj
new file mode 100644
index 000000000..24b3ecaab
--- /dev/null
+++ b/src/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\SharedVersion.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Mono.Nat" />
+ </ItemGroup>
+</Project>
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
new file mode 100644
index 000000000..1da44b048
--- /dev/null
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -0,0 +1,1125 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
+using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
+
+namespace Jellyfin.Networking.Manager;
+
+/// <summary>
+/// Class to take care of network interface management.
+/// </summary>
+public class NetworkManager : INetworkManager, IDisposable
+{
+ /// <summary>
+ /// Threading lock for network properties.
+ /// </summary>
+ private readonly object _initLock;
+
+ private readonly ILogger<NetworkManager> _logger;
+
+ private readonly IConfigurationManager _configurationManager;
+
+ private readonly IConfiguration _startupConfig;
+
+ private readonly object _networkEventLock;
+
+ /// <summary>
+ /// Holds the published server URLs and the IPs to use them on.
+ /// </summary>
+ private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
+
+ private IReadOnlyList<IPNetwork> _remoteAddressFilter;
+
+ /// <summary>
+ /// Used to stop "event-racing conditions".
+ /// </summary>
+ private bool _eventfire;
+
+ /// <summary>
+ /// List of all interface MAC addresses.
+ /// </summary>
+ private IReadOnlyList<PhysicalAddress> _macAddresses;
+
+ /// <summary>
+ /// Dictionary containing interface addresses and their subnets.
+ /// </summary>
+ private IReadOnlyList<IPData> _interfaces;
+
+ /// <summary>
+ /// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>)
+ /// or internal interface network subnets if undefined by user.
+ /// </summary>
+ private IReadOnlyList<IPNetwork> _lanSubnets;
+
+ /// <summary>
+ /// User defined list of subnets to excluded from the LAN.
+ /// </summary>
+ private IReadOnlyList<IPNetwork> _excludedSubnets;
+
+ /// <summary>
+ /// True if this object is disposed.
+ /// </summary>
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NetworkManager"/> class.
+ /// </summary>
+ /// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param>
+ /// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param>
+ /// <param name="logger">Logger to use for messages.</param>
+#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
+ public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger)
+ {
+ ArgumentNullException.ThrowIfNull(logger);
+ ArgumentNullException.ThrowIfNull(configurationManager);
+
+ _logger = logger;
+ _configurationManager = configurationManager;
+ _startupConfig = startupConfig;
+ _initLock = new();
+ _interfaces = new List<IPData>();
+ _macAddresses = new List<PhysicalAddress>();
+ _publishedServerUrls = new List<PublishedServerUriOverride>();
+ _networkEventLock = new object();
+ _remoteAddressFilter = new List<IPNetwork>();
+
+ UpdateSettings(_configurationManager.GetNetworkConfiguration());
+
+ NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
+ NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
+
+ _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated;
+ }
+#pragma warning restore CS8618 // Non-nullable field is uninitialized.
+
+ /// <summary>
+ /// Event triggered on network changes.
+ /// </summary>
+ public event EventHandler? NetworkChanged;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether testing is taking place.
+ /// </summary>
+ public static string MockNetworkSettings { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets a value indicating whether IP4 is enabled.
+ /// </summary>
+ public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4;
+
+ /// <summary>
+ /// Gets a value indicating whether IP6 is enabled.
+ /// </summary>
+ public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6;
+
+ /// <summary>
+ /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
+ /// </summary>
+ public bool TrustAllIPv6Interfaces { get; private set; }
+
+ /// <summary>
+ /// Gets the Published server override list.
+ /// </summary>
+ public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Handler for network change events.
+ /// </summary>
+ /// <param name="sender">Sender.</param>
+ /// <param name="e">A <see cref="NetworkAvailabilityEventArgs"/> containing network availability information.</param>
+ private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e)
+ {
+ _logger.LogDebug("Network availability changed.");
+ HandleNetworkChange();
+ }
+
+ /// <summary>
+ /// Handler for network change events.
+ /// </summary>
+ /// <param name="sender">Sender.</param>
+ /// <param name="e">An <see cref="EventArgs"/>.</param>
+ private void OnNetworkAddressChanged(object? sender, EventArgs e)
+ {
+ _logger.LogDebug("Network address change detected.");
+ HandleNetworkChange();
+ }
+
+ /// <summary>
+ /// Triggers our event, and re-loads interface information.
+ /// </summary>
+ private void HandleNetworkChange()
+ {
+ lock (_networkEventLock)
+ {
+ if (!_eventfire)
+ {
+ // As network events tend to fire one after the other only fire once every second.
+ _eventfire = true;
+ OnNetworkChange();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession.
+ /// </summary>
+ private void OnNetworkChange()
+ {
+ try
+ {
+ Thread.Sleep(2000);
+ var networkConfig = _configurationManager.GetNetworkConfiguration();
+ if (IsIPv6Enabled && !Socket.OSSupportsIPv6)
+ {
+ UpdateSettings(networkConfig);
+ }
+ else
+ {
+ InitializeInterfaces();
+ InitializeLan(networkConfig);
+ EnforceBindSettings(networkConfig);
+ }
+
+ PrintNetworkInformation(networkConfig);
+ NetworkChanged?.Invoke(this, EventArgs.Empty);
+ }
+ finally
+ {
+ _eventfire = false;
+ }
+ }
+
+ /// <summary>
+ /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
+ /// Generate a list of all active mac addresses that aren't loopback addresses.
+ /// </summary>
+ private void InitializeInterfaces()
+ {
+ lock (_initLock)
+ {
+ _logger.LogDebug("Refreshing interfaces.");
+
+ var interfaces = new List<IPData>();
+ var macAddresses = new List<PhysicalAddress>();
+
+ try
+ {
+ var nics = NetworkInterface.GetAllNetworkInterfaces()
+ .Where(i => i.OperationalStatus == OperationalStatus.Up);
+
+ foreach (NetworkInterface adapter in nics)
+ {
+ try
+ {
+ var ipProperties = adapter.GetIPProperties();
+ var mac = adapter.GetPhysicalAddress();
+
+ // Populate MAC list
+ if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac))
+ {
+ macAddresses.Add(mac);
+ }
+
+ // Populate interface list
+ foreach (var info in ipProperties.UnicastAddresses)
+ {
+ if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
+ {
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv4Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
+
+ interfaces.Add(interfaceObject);
+ }
+ else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv6Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
+
+ interfaces.Add(interfaceObject);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // Ignore error, and attempt to continue.
+ _logger.LogError(ex, "Error encountered parsing interfaces.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error obtaining interfaces.");
+ }
+
+ // If no interfaces are found, fallback to loopback interfaces.
+ if (interfaces.Count == 0)
+ {
+ _logger.LogWarning("No interface information available. Using loopback interface(s).");
+
+ if (IsIPv4Enabled)
+ {
+ interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
+ }
+
+ if (IsIPv6Enabled)
+ {
+ interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
+ }
+ }
+
+ _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count);
+ _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString()));
+
+ _macAddresses = macAddresses;
+ _interfaces = interfaces;
+ }
+ }
+
+ /// <summary>
+ /// Initializes internal LAN cache.
+ /// </summary>
+ private void InitializeLan(NetworkConfiguration config)
+ {
+ lock (_initLock)
+ {
+ _logger.LogDebug("Refreshing LAN information.");
+
+ // Get configuration options
+ var subnets = config.LocalNetworkSubnets;
+
+ // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN
+ if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0)
+ {
+ _logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
+
+ var fallbackLanSubnets = new List<IPNetwork>();
+ if (IsIPv6Enabled)
+ {
+ fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291Loopback); // RFC 4291 (Loopback)
+ fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291SiteLocal); // RFC 4291 (Site local)
+ fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local)
+ }
+
+ if (IsIPv4Enabled)
+ {
+ fallbackLanSubnets.Add(NetworkConstants.IPv4RFC5735Loopback); // RFC 5735 (Loopback)
+ fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A)
+ fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B)
+ fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C)
+ }
+
+ _lanSubnets = fallbackLanSubnets;
+ }
+ else
+ {
+ _lanSubnets = lanSubnets;
+ }
+
+ _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true)
+ ? excludedSubnets
+ : new List<IPNetwork>();
+ }
+ }
+
+ /// <summary>
+ /// Enforce bind addresses and exclusions on available interfaces.
+ /// </summary>
+ private void EnforceBindSettings(NetworkConfiguration config)
+ {
+ lock (_initLock)
+ {
+ // Respect explicit bind addresses
+ var interfaces = _interfaces.ToList();
+ var localNetworkAddresses = config.LocalNetworkAddresses;
+ if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]))
+ {
+ var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network)
+ ? network.Prefix
+ : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase))
+ .Select(x => x.Address)
+ .FirstOrDefault() ?? IPAddress.None))
+ .Where(x => x != IPAddress.None)
+ .ToHashSet();
+ interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
+
+ if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
+ {
+ interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
+ }
+
+ if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
+ {
+ interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
+ }
+ }
+
+ // Remove all interfaces matching any virtual machine interface prefix
+ if (config.IgnoreVirtualInterfaces)
+ {
+ // Remove potentially existing * and split config string into prefixes
+ var virtualInterfacePrefixes = config.VirtualInterfaceNames
+ .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase));
+
+ // Check all interfaces for matches against the prefixes and remove them
+ if (_interfaces.Count > 0)
+ {
+ foreach (var virtualInterfacePrefix in virtualInterfacePrefixes)
+ {
+ interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+ }
+
+ // Remove all IPv4 interfaces if IPv4 is disabled
+ if (!IsIPv4Enabled)
+ {
+ interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork);
+ }
+
+ // Remove all IPv6 interfaces if IPv6 is disabled
+ if (!IsIPv6Enabled)
+ {
+ interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
+ }
+
+ _interfaces = interfaces;
+ }
+ }
+
+ /// <summary>
+ /// Initializes the remote address values.
+ /// </summary>
+ private void InitializeRemote(NetworkConfiguration config)
+ {
+ lock (_initLock)
+ {
+ // Parse config values into filter collection
+ var remoteIPFilter = config.RemoteIPFilter;
+ if (remoteIPFilter.Length != 0 && !string.IsNullOrWhiteSpace(remoteIPFilter[0]))
+ {
+ // Parse all IPs with netmask to a subnet
+ var remoteAddressFilter = new List<IPNetwork>();
+ var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray();
+ if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false))
+ {
+ remoteAddressFilter = remoteAddressFilterResult.ToList();
+ }
+
+ // Parse everything else as an IP and construct subnet with a single IP
+ var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase));
+ foreach (var ip in remoteFilteredIPs)
+ {
+ if (IPAddress.TryParse(ip, out var ipp))
+ {
+ remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize));
+ }
+ }
+
+ _remoteAddressFilter = remoteAddressFilter;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Parses the user defined overrides into the dictionary object.
+ /// Overrides are the equivalent of localised publishedServerUrl, enabling
+ /// different addresses to be advertised over different subnets.
+ /// format is subnet=ipaddress|host|uri
+ /// when subnet = 0.0.0.0, any external address matches.
+ /// </summary>
+ private void InitializeOverrides(NetworkConfiguration config)
+ {
+ lock (_initLock)
+ {
+ var publishedServerUrls = new List<PublishedServerUriOverride>();
+
+ // Prefer startup configuration.
+ var startupOverrideKey = _startupConfig[AddressOverrideKey];
+ if (!string.IsNullOrEmpty(startupOverrideKey))
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
+ startupOverrideKey,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
+ startupOverrideKey,
+ true,
+ true));
+ _publishedServerUrls = publishedServerUrls;
+ return;
+ }
+
+ var overrides = config.PublishedServerUriBySubnet;
+ foreach (var entry in overrides)
+ {
+ var parts = entry.Split('=');
+ if (parts.Length != 2)
+ {
+ _logger.LogError("Unable to parse bind override: {Entry}", entry);
+ return;
+ }
+
+ var replacement = parts[1].Trim();
+ var identifier = parts[0];
+ if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
+ {
+ // Drop any other overrides in case an "all" override exists
+ publishedServerUrls.Clear();
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
+ replacement,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
+ replacement,
+ true,
+ true));
+ break;
+ }
+ else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
+ replacement,
+ false,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
+ replacement,
+ false,
+ true));
+ }
+ else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var lan in _lanSubnets)
+ {
+ var lanPrefix = lan.Prefix;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
+ replacement,
+ true,
+ false));
+ }
+ }
+ else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null)
+ {
+ var data = new IPData(result.Prefix, result);
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ data,
+ replacement,
+ true,
+ true));
+ }
+ else if (TryParseInterface(identifier, out var ifaces))
+ {
+ foreach (var iface in ifaces)
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ iface,
+ replacement,
+ true,
+ true));
+ }
+ }
+ else
+ {
+ _logger.LogError("Unable to parse bind override: {Entry}", entry);
+ }
+ }
+
+ _publishedServerUrls = publishedServerUrls;
+ }
+ }
+
+ private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
+ {
+ if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
+ {
+ UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
+ }
+ }
+
+ /// <summary>
+ /// Reloads all settings and re-Initializes the instance.
+ /// </summary>
+ /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
+ public void UpdateSettings(object configuration)
+ {
+ ArgumentNullException.ThrowIfNull(configuration);
+
+ var config = (NetworkConfiguration)configuration;
+ HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
+
+ InitializeLan(config);
+ InitializeRemote(config);
+
+ if (string.IsNullOrEmpty(MockNetworkSettings))
+ {
+ InitializeInterfaces();
+ }
+ else // Used in testing only.
+ {
+ // Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway.
+ var interfaceList = MockNetworkSettings.Split('|');
+ var interfaces = new List<IPData>();
+ foreach (var details in interfaceList)
+ {
+ var parts = details.Split(',');
+ if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet))
+ {
+ var address = subnet.Prefix;
+ var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
+ if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ var data = new IPData(address, subnet, parts[2])
+ {
+ Index = index
+ };
+ interfaces.Add(data);
+ }
+ }
+ else
+ {
+ _logger.LogWarning("Could not parse mock interface settings: {Part}", details);
+ }
+ }
+
+ _interfaces = interfaces;
+ }
+
+ EnforceBindSettings(config);
+ InitializeOverrides(config);
+
+ PrintNetworkInformation(config, false);
+ }
+
+ /// <summary>
+ /// Protected implementation of Dispose pattern.
+ /// </summary>
+ /// <param name="disposing"><c>True</c> to dispose the managed state.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated;
+ NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
+ NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged;
+ }
+
+ _disposed = true;
+ }
+ }
+
+ /// <inheritdoc/>
+ public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result)
+ {
+ if (string.IsNullOrEmpty(intf)
+ || _interfaces is null
+ || _interfaces.Count == 0)
+ {
+ result = null;
+ return false;
+ }
+
+ // Match all interfaces starting with names starting with token
+ result = _interfaces
+ .Where(i => i.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)
+ && ((IsIPv4Enabled && i.Address.AddressFamily == AddressFamily.InterNetwork)
+ || (IsIPv6Enabled && i.Address.AddressFamily == AddressFamily.InterNetworkV6)))
+ .OrderBy(x => x.Index)
+ .ToArray();
+ return result.Count > 0;
+ }
+
+ /// <inheritdoc/>
+ public bool HasRemoteAccess(IPAddress remoteIP)
+ {
+ var config = _configurationManager.GetNetworkConfiguration();
+ if (config.EnableRemoteAccess)
+ {
+ // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
+ // If left blank, all remote addresses will be allowed.
+ if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
+ {
+ // remoteAddressFilter is a whitelist or blacklist.
+ var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
+ if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
+ || (config.IsRemoteIPFilterBlacklist && matches == 0))
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+ else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
+ {
+ // Remote not enabled. So everyone should be LAN.
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<PhysicalAddress> GetMacAddresses()
+ {
+ // Populated in construction - so always has values.
+ return _macAddresses;
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<IPData> GetLoopbacks()
+ {
+ if (!IsIPv4Enabled && !IsIPv6Enabled)
+ {
+ return Array.Empty<IPData>();
+ }
+
+ var loopbackNetworks = new List<IPData>();
+ if (IsIPv4Enabled)
+ {
+ loopbackNetworks.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
+ }
+
+ if (IsIPv6Enabled)
+ {
+ loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
+ }
+
+ return loopbackNetworks;
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
+ {
+ if (_interfaces.Count > 0 || individualInterfaces)
+ {
+ return _interfaces;
+ }
+
+ // No bind address and no exclusions, so listen on all interfaces.
+ var result = new List<IPData>();
+ if (IsIPv4Enabled && IsIPv6Enabled)
+ {
+ // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
+ result.Add(new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any));
+ }
+ else if (IsIPv4Enabled)
+ {
+ result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any));
+ }
+ else if (IsIPv6Enabled)
+ {
+ // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too.
+ foreach (var iface in _interfaces)
+ {
+ if (iface.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ result.Add(iface);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public string GetBindAddress(string source, out int? port)
+ {
+ if (!NetworkUtils.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
+ {
+ addresses = Array.Empty<IPAddress>();
+ }
+
+ var result = GetBindAddress(addresses.FirstOrDefault(), out port);
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public string GetBindAddress(HttpRequest source, out int? port)
+ {
+ var result = GetBindAddress(source.Host.Host, out port);
+ port ??= source.Host.Port;
+
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false)
+ {
+ port = null;
+
+ string result;
+
+ if (source is not null)
+ {
+ if (IsIPv4Enabled && !IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
+ }
+
+ if (!IsIPv4Enabled && IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetwork)
+ {
+ _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
+ }
+
+ bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
+ _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
+
+ if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
+ {
+ return result;
+ }
+
+ // No preference given, so move on to bind addresses.
+ if (MatchesBindInterface(source, isExternal, out result))
+ {
+ return result;
+ }
+
+ if (isExternal && MatchesExternalInterface(source, out result))
+ {
+ return result;
+ }
+ }
+
+ // Get the first LAN interface address that's not excluded and not a loopback address.
+ // Get all available interfaces, prefer local interfaces
+ var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address))
+ .OrderByDescending(x => IsInLocalNetwork(x.Address))
+ .ThenBy(x => x.Index)
+ .ToList();
+
+ if (availableInterfaces.Count == 0)
+ {
+ // There isn't any others, so we'll use the loopback.
+ result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1";
+ _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
+ return result;
+ }
+
+ // If no source address is given, use the preferred (first) interface
+ if (source is null)
+ {
+ result = NetworkUtils.FormatIPString(availableInterfaces.First().Address);
+ _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result);
+ return result;
+ }
+
+ // Does the request originate in one of the interface subnets?
+ // (For systems with multiple internal network cards, and multiple subnets)
+ foreach (var intf in availableInterfaces)
+ {
+ if (intf.Subnet.Contains(source))
+ {
+ result = NetworkUtils.FormatIPString(intf.Address);
+ _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
+ return result;
+ }
+ }
+
+ // Fallback to first available interface
+ result = NetworkUtils.FormatIPString(availableInterfaces[0].Address);
+ _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result);
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<IPData> GetInternalBindAddresses()
+ {
+ // Select all local bind addresses
+ return _interfaces.Where(x => IsInLocalNetwork(x.Address))
+ .OrderBy(x => x.Index)
+ .ToList();
+ }
+
+ /// <inheritdoc/>
+ public bool IsInLocalNetwork(string address)
+ {
+ if (NetworkUtils.TryParseToSubnet(address, out var subnet))
+ {
+ return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
+ }
+
+ if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
+ {
+ foreach (var ept in addresses)
+ {
+ if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public bool IsInLocalNetwork(IPAddress address)
+ {
+ ArgumentNullException.ThrowIfNull(address);
+
+ // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
+ if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
+ || address.Equals(IPAddress.Loopback)
+ || address.Equals(IPAddress.IPv6Loopback))
+ {
+ return true;
+ }
+
+ // As private addresses can be redefined by Configuration.LocalNetworkAddresses
+ return CheckIfLanAndNotExcluded(address);
+ }
+
+ private bool CheckIfLanAndNotExcluded(IPAddress address)
+ {
+ foreach (var lanSubnet in _lanSubnets)
+ {
+ if (lanSubnet.Contains(address))
+ {
+ foreach (var excludedSubnet in _excludedSubnets)
+ {
+ if (excludedSubnet.Contains(address))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Attempts to match the source against the published server URL overrides.
+ /// </summary>
+ /// <param name="source">IP source address to use.</param>
+ /// <param name="isInExternalSubnet">True if the source is in an external subnet.</param>
+ /// <param name="bindPreference">The published server URL that matches the source address.</param>
+ /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
+ private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference)
+ {
+ bindPreference = string.Empty;
+ int? port = null;
+
+ // Only consider subnets including the source IP, prefering specific overrides
+ List<PublishedServerUriOverride> validPublishedServerUrls;
+ if (!isInExternalSubnet)
+ {
+ // Only use matching internal subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+ .ToList();
+ }
+ else
+ {
+ // Only use matching external subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+ .ToList();
+ }
+
+ foreach (var data in validPublishedServerUrls)
+ {
+ // Get interface matching override subnet
+ var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
+
+ if (intf?.Address is not null)
+ {
+ // If matching interface is found, use override
+ bindPreference = data.OverrideUri;
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty(bindPreference))
+ {
+ _logger.LogDebug("{Source}: No matching bind address override found", source);
+ return false;
+ }
+
+ // Handle override specifying port
+ var parts = bindPreference.Split(':');
+ if (parts.Length > 1)
+ {
+ if (int.TryParse(parts[1], out int p))
+ {
+ bindPreference = parts[0];
+ port = p;
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
+ return true;
+ }
+ }
+
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
+ return true;
+ }
+
+ /// <summary>
+ /// Attempts to match the source against the user defined bind interfaces.
+ /// </summary>
+ /// <param name="source">IP source address to use.</param>
+ /// <param name="isInExternalSubnet">True if the source is in the external subnet.</param>
+ /// <param name="result">The result, if a match is found.</param>
+ /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
+ private bool MatchesBindInterface(IPAddress source, bool isInExternalSubnet, out string result)
+ {
+ result = string.Empty;
+
+ int count = _interfaces.Count;
+ if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any)))
+ {
+ // Ignore IPAny addresses.
+ count = 0;
+ }
+
+ if (count == 0)
+ {
+ return false;
+ }
+
+ IPAddress? bindAddress = null;
+ if (isInExternalSubnet)
+ {
+ var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
+ .OrderBy(x => x.Index)
+ .ToList();
+ if (externalInterfaces.Count > 0)
+ {
+ // Check to see if any of the external bind interfaces are in the same subnet as the source.
+ // If none exists, this will select the first external interface if there is one.
+ bindAddress = externalInterfaces
+ .OrderByDescending(x => x.Subnet.Contains(source))
+ .ThenBy(x => x.Index)
+ .Select(x => x.Address)
+ .First();
+
+ result = NetworkUtils.FormatIPString(bindAddress);
+ _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result);
+ return true;
+ }
+
+ _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source);
+ }
+ else
+ {
+ // Check to see if any of the internal bind interfaces are in the same subnet as the source.
+ // If none exists, this will select the first internal interface if there is one.
+ bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
+ .OrderByDescending(x => x.Subnet.Contains(source))
+ .ThenBy(x => x.Index)
+ .Select(x => x.Address)
+ .FirstOrDefault();
+
+ if (bindAddress is not null)
+ {
+ result = NetworkUtils.FormatIPString(bindAddress);
+ _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Attempts to match the source against external interfaces.
+ /// </summary>
+ /// <param name="source">IP source address to use.</param>
+ /// <param name="result">The result, if a match is found.</param>
+ /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
+ private bool MatchesExternalInterface(IPAddress source, out string result)
+ {
+ // Get the first external interface address that isn't a loopback.
+ var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray();
+
+ // No external interface found
+ if (extResult.Length == 0)
+ {
+ result = string.Empty;
+ _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source);
+ return false;
+ }
+
+ // Does the request originate in one of the interface subnets?
+ // (For systems with multiple network cards and/or multiple subnets)
+ foreach (var intf in extResult)
+ {
+ if (intf.Subnet.Contains(source))
+ {
+ result = NetworkUtils.FormatIPString(intf.Address);
+ _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
+ return true;
+ }
+ }
+
+ // Fallback to first external interface.
+ result = NetworkUtils.FormatIPString(extResult[0].Address);
+ _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
+ return true;
+ }
+
+ private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
+ {
+ var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
+ if (_logger.IsEnabled(logLevel))
+ {
+ _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
+ _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
+ _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
+ }
+ }
+}
diff --git a/src/Jellyfin.Networking/Udp/SocketFactory.cs b/src/Jellyfin.Networking/Udp/SocketFactory.cs
new file mode 100644
index 000000000..f0267debc
--- /dev/null
+++ b/src/Jellyfin.Networking/Udp/SocketFactory.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using MediaBrowser.Model.Net;
+
+namespace Jellyfin.Networking.Udp;
+
+/// <summary>
+/// Factory class to create different kinds of sockets.
+/// </summary>
+public class SocketFactory : ISocketFactory
+{
+ /// <inheritdoc />
+ public Socket CreateUdpBroadcastSocket(int localPort)
+ {
+ if (localPort < 0)
+ {
+ throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
+ }
+
+ var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ try
+ {
+ socket.EnableBroadcast = true;
+ socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
+ socket.Bind(new IPEndPoint(IPAddress.Any, localPort));
+
+ return socket;
+ }
+ catch
+ {
+ socket.Dispose();
+
+ throw;
+ }
+ }
+}
diff --git a/src/Jellyfin.Networking/Udp/UdpServer.cs b/src/Jellyfin.Networking/Udp/UdpServer.cs
new file mode 100644
index 000000000..b130a5a5f
--- /dev/null
+++ b/src/Jellyfin.Networking/Udp/UdpServer.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.ApiClient;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+
+namespace Jellyfin.Networking.Udp;
+
+/// <summary>
+/// Provides a Udp Server.
+/// </summary>
+public sealed class UdpServer : IDisposable
+{
+ /// <summary>
+ /// The _logger.
+ /// </summary>
+ private readonly ILogger _logger;
+ private readonly IServerApplicationHost _appHost;
+ private readonly IConfiguration _config;
+
+ private readonly byte[] _receiveBuffer = new byte[8192];
+
+ private readonly Socket _udpSocket;
+ private readonly IPEndPoint _endpoint;
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UdpServer" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="configuration">The configuration manager.</param>
+ /// <param name="bindAddress"> The bind address.</param>
+ /// <param name="port">The port.</param>
+ public UdpServer(
+ ILogger logger,
+ IServerApplicationHost appHost,
+ IConfiguration configuration,
+ IPAddress bindAddress,
+ int port)
+ {
+ _logger = logger;
+ _appHost = appHost;
+ _config = configuration;
+
+ _endpoint = new IPEndPoint(bindAddress, port);
+
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
+ {
+ MulticastLoopback = false,
+ };
+ _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ }
+
+ private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken)
+ {
+ string? localUrl = _config[AddressOverrideKey];
+ if (string.IsNullOrEmpty(localUrl))
+ {
+ localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
+ }
+
+ if (string.IsNullOrEmpty(localUrl))
+ {
+ _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined.");
+ return;
+ }
+
+ var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+
+ try
+ {
+ _logger.LogDebug("Sending AutoDiscovery response");
+ await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogError(ex, "Error sending response message");
+ }
+ }
+
+ /// <summary>
+ /// Starts the specified port.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ public void Start(CancellationToken cancellationToken)
+ {
+ _udpSocket.Bind(_endpoint);
+
+ _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task BeginReceiveAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
+ var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
+ var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
+ if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
+ {
+ await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogError(ex, "Failed to receive data from socket");
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogDebug("Broadcast socket operation cancelled");
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _udpSocket.Dispose();
+ _disposed = true;
+ }
+}
diff --git a/src/Jellyfin.Networking/UdpServerEntryPoint.cs b/src/Jellyfin.Networking/UdpServerEntryPoint.cs
new file mode 100644
index 000000000..61180c3c0
--- /dev/null
+++ b/src/Jellyfin.Networking/UdpServerEntryPoint.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Udp;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
+
+namespace Jellyfin.Networking;
+
+/// <summary>
+/// Class responsible for registering all UDP broadcast endpoints and their handlers.
+/// </summary>
+public sealed class UdpServerEntryPoint : IServerEntryPoint
+{
+ /// <summary>
+ /// The port of the UDP server.
+ /// </summary>
+ public const int PortNumber = 7359;
+
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<UdpServerEntryPoint> _logger;
+ private readonly IServerApplicationHost _appHost;
+ private readonly IConfiguration _config;
+ private readonly IConfigurationManager _configurationManager;
+ private readonly INetworkManager _networkManager;
+
+ /// <summary>
+ /// The UDP server.
+ /// </summary>
+ private readonly List<UdpServer> _udpServers;
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
+ /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ public UdpServerEntryPoint(
+ ILogger<UdpServerEntryPoint> logger,
+ IServerApplicationHost appHost,
+ IConfiguration configuration,
+ IConfigurationManager configurationManager,
+ INetworkManager networkManager)
+ {
+ _logger = logger;
+ _appHost = appHost;
+ _config = configuration;
+ _configurationManager = configurationManager;
+ _networkManager = networkManager;
+ _udpServers = new List<UdpServer>();
+ }
+
+ /// <inheritdoc />
+ public Task RunAsync()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery)
+ {
+ return Task.CompletedTask;
+ }
+
+ try
+ {
+ // Linux needs to bind to the broadcast addresses to get broadcast traffic
+ // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
+ if (OperatingSystem.IsLinux())
+ {
+ // Add global broadcast listener
+ var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
+ server.Start(_cancellationTokenSource.Token);
+ _udpServers.Add(server);
+
+ // Add bind address specific broadcast listeners
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+ foreach (var intf in validInterfaces)
+ {
+ var broadcastAddress = NetworkUtils.GetBroadcastAddress(intf.Subnet);
+ _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
+
+ server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
+ server.Start(_cancellationTokenSource.Token);
+ _udpServers.Add(server);
+ }
+ }
+ else
+ {
+ // Add bind address specific broadcast listeners
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+ foreach (var intf in validInterfaces)
+ {
+ var intfAddress = intf.Address;
+ _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
+
+ var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
+ server.Start(_cancellationTokenSource.Token);
+ _udpServers.Add(server);
+ }
+ }
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _cancellationTokenSource.Cancel();
+ _cancellationTokenSource.Dispose();
+ foreach (var server in _udpServers)
+ {
+ server.Dispose();
+ }
+
+ _udpServers.Clear();
+ _disposed = true;
+ }
+}
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index de8fc1bb8..bec3481cb 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -4,7 +4,7 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
+ <TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs
new file mode 100644
index 000000000..c7331c718
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoFixture.Xunit2;
+using Jellyfin.Api.Controllers;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.QuickConnect;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Users;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Nikse.SubtitleEdit.Core.Common;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Controllers;
+
+public class UserControllerTests
+{
+ private readonly UserController _subject;
+ private readonly Mock<IUserManager> _mockUserManager;
+ private readonly Mock<ISessionManager> _mockSessionManager;
+ private readonly Mock<INetworkManager> _mockNetworkManager;
+ private readonly Mock<IDeviceManager> _mockDeviceManager;
+ private readonly Mock<IAuthorizationContext> _mockAuthorizationContext;
+ private readonly Mock<IServerConfigurationManager> _mockServerConfigurationManager;
+ private readonly Mock<ILogger<UserController>> _mockLogger;
+ private readonly Mock<IQuickConnect> _mockQuickConnect;
+ private readonly Mock<IPlaylistManager> _mockPlaylistManager;
+
+ public UserControllerTests()
+ {
+ _mockUserManager = new Mock<IUserManager>();
+ _mockSessionManager = new Mock<ISessionManager>();
+ _mockNetworkManager = new Mock<INetworkManager>();
+ _mockDeviceManager = new Mock<IDeviceManager>();
+ _mockAuthorizationContext = new Mock<IAuthorizationContext>();
+ _mockServerConfigurationManager = new Mock<IServerConfigurationManager>();
+ _mockLogger = new Mock<ILogger<UserController>>();
+ _mockQuickConnect = new Mock<IQuickConnect>();
+ _mockPlaylistManager = new Mock<IPlaylistManager>();
+
+ _subject = new UserController(
+ _mockUserManager.Object,
+ _mockSessionManager.Object,
+ _mockNetworkManager.Object,
+ _mockDeviceManager.Object,
+ _mockAuthorizationContext.Object,
+ _mockServerConfigurationManager.Object,
+ _mockLogger.Object,
+ _mockQuickConnect.Object,
+ _mockPlaylistManager.Object);
+ }
+
+ [Theory]
+ [AutoData]
+ public async Task UpdateUserPolicy_WhenUserNotFound_ReturnsNotFound(Guid userId, UserPolicy userPolicy)
+ {
+ User? nullUser = null;
+ _mockUserManager.
+ Setup(m => m.GetUserById(userId))
+ .Returns(nullUser);
+
+ Assert.IsType<NotFoundResult>(await _subject.UpdateUserPolicy(userId, userPolicy));
+ }
+
+ [Theory]
+ [InlineAutoData(null)]
+ [InlineAutoData("")]
+ [InlineAutoData(" ")]
+ public void UpdateUserPolicy_WhenPasswordResetProviderIdNotSupplied_ReturnsBadRequest(string? passwordResetProviderId)
+ {
+ var userPolicy = new UserPolicy
+ {
+ PasswordResetProviderId = passwordResetProviderId,
+ AuthenticationProviderId = "AuthenticationProviderId"
+ };
+
+ Assert.Contains(
+ Validate(userPolicy), v =>
+ v.MemberNames.Contains("PasswordResetProviderId") &&
+ v.ErrorMessage != null &&
+ v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase));
+ }
+
+ [Theory]
+ [InlineAutoData(null)]
+ [InlineAutoData("")]
+ [InlineAutoData(" ")]
+ public void UpdateUserPolicy_WhenAuthenticationProviderIdNotSupplied_ReturnsBadRequest(string? authenticationProviderId)
+ {
+ var userPolicy = new UserPolicy
+ {
+ AuthenticationProviderId = authenticationProviderId,
+ PasswordResetProviderId = "PasswordResetProviderId"
+ };
+
+ Assert.Contains(Validate(userPolicy), v =>
+ v.MemberNames.Contains("AuthenticationProviderId") &&
+ v.ErrorMessage != null &&
+ v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase));
+ }
+
+ private List<ValidationResult> Validate(object model)
+ {
+ var result = new List<ValidationResult>();
+ var context = new ValidationContext(model, null, null);
+ Validator.TryValidateObject(model, context, result, true);
+
+ return result;
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
index 2d7741d81..a2d1b3607 100644
--- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
+++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
@@ -14,7 +14,7 @@ namespace Jellyfin.Api.Tests.Helpers
{
[Theory]
[MemberData(nameof(GetOrderBy_Success_TestData))]
- public static void GetOrderBy_Success(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder, (string, SortOrder)[] expected)
+ public static void GetOrderBy_Success(IReadOnlyList<ItemSortBy> sortBy, IReadOnlyList<SortOrder> requestedSortOrder, (ItemSortBy, SortOrder)[] expected)
{
Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
}
@@ -95,42 +95,42 @@ namespace Jellyfin.Api.Tests.Helpers
Assert.Throws<SecurityException>(() => RequestHelpers.GetUserId(principal, requestUserId));
}
- public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData()
+ public static TheoryData<IReadOnlyList<ItemSortBy>, IReadOnlyList<SortOrder>, (ItemSortBy, SortOrder)[]> GetOrderBy_Success_TestData()
{
- var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>();
+ var data = new TheoryData<IReadOnlyList<ItemSortBy>, IReadOnlyList<SortOrder>, (ItemSortBy, SortOrder)[]>();
data.Add(
- Array.Empty<string>(),
+ Array.Empty<ItemSortBy>(),
Array.Empty<SortOrder>(),
- Array.Empty<(string, SortOrder)>());
+ Array.Empty<(ItemSortBy, SortOrder)>());
data.Add(
- new string[]
+ new[]
{
- "IsFavoriteOrLiked",
- "Random"
+ ItemSortBy.IsFavoriteOrLiked,
+ ItemSortBy.Random
},
Array.Empty<SortOrder>(),
- new (string, SortOrder)[]
+ new (ItemSortBy, SortOrder)[]
{
- ("IsFavoriteOrLiked", SortOrder.Ascending),
- ("Random", SortOrder.Ascending),
+ (ItemSortBy.IsFavoriteOrLiked, SortOrder.Ascending),
+ (ItemSortBy.Random, SortOrder.Ascending),
});
data.Add(
- new string[]
+ new[]
{
- "SortName",
- "ProductionYear"
+ ItemSortBy.SortName,
+ ItemSortBy.ProductionYear
},
- new SortOrder[]
+ new[]
{
SortOrder.Descending
},
- new (string, SortOrder)[]
+ new (ItemSortBy, SortOrder)[]
{
- ("SortName", SortOrder.Descending),
- ("ProductionYear", SortOrder.Descending),
+ (ItemSortBy.SortName, SortOrder.Descending),
+ (ItemSortBy.ProductionYear, SortOrder.Descending),
});
return data;
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs
index 93e065685..1ff7e7b7a 100644
--- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
+++ b/tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs
@@ -1,12 +1,11 @@
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Api.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using Xunit;
-namespace Jellyfin.Server.Tests
+namespace Jellyfin.Api.Middleware.Tests
{
public static class UrlDecodeQueryFeatureTests
{
diff --git a/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs b/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs
deleted file mode 100644
index 78a956f5f..000000000
--- a/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using Emby.Dlna;
-using Emby.Dlna.PlayTo;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-using Moq;
-using Xunit;
-
-namespace Jellyfin.Dlna.Tests
-{
- public class DlnaManagerTests
- {
- private DlnaManager GetManager()
- {
- var xmlSerializer = new Mock<IXmlSerializer>();
- var fileSystem = new Mock<IFileSystem>();
- var appPaths = new Mock<IApplicationPaths>();
- var loggerFactory = new Mock<ILoggerFactory>();
- var appHost = new Mock<IServerApplicationHost>();
-
- return new DlnaManager(xmlSerializer.Object, fileSystem.Object, appPaths.Object, loggerFactory.Object, appHost.Object);
- }
-
- [Fact]
- public void IsMatch_GivenMatchingName_ReturnsTrue()
- {
- var device = new DeviceInfo()
- {
- Name = "My Device",
- Manufacturer = "LG Electronics",
- ManufacturerUrl = "http://www.lge.com",
- ModelDescription = "LG WebOSTV DMRplus",
- ModelName = "LG TV",
- ModelNumber = "1.0",
- };
-
- var profile = new DeviceProfile()
- {
- Name = "Test Profile",
- FriendlyName = "My Device",
- Manufacturer = "LG Electronics",
- ManufacturerUrl = "http://www.lge.com",
- ModelDescription = "LG WebOSTV DMRplus",
- ModelName = "LG TV",
- ModelNumber = "1.0",
- Identification = new()
- {
- FriendlyName = "My Device",
- Manufacturer = "LG Electronics",
- ManufacturerUrl = "http://www.lge.com",
- ModelDescription = "LG WebOSTV DMRplus",
- ModelName = "LG TV",
- ModelNumber = "1.0",
- }
- };
-
- var profile2 = new DeviceProfile()
- {
- Name = "Test Profile",
- FriendlyName = "My Device",
- Identification = new DeviceIdentification()
- {
- FriendlyName = "My Device",
- }
- };
-
- var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile2.Identification);
- var deviceMatch2 = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification);
-
- Assert.True(deviceMatch);
- Assert.True(deviceMatch2);
- }
-
- [Fact]
- public void IsMatch_GivenNamesAndManufacturersDoNotMatch_ReturnsFalse()
- {
- var device = new DeviceInfo()
- {
- Name = "My Device",
- Manufacturer = "JVC"
- };
-
- var profile = new DeviceProfile()
- {
- Name = "Test Profile",
- FriendlyName = "My Device",
- Manufacturer = "LG Electronics",
- ManufacturerUrl = "http://www.lge.com",
- ModelDescription = "LG WebOSTV DMRplus",
- ModelName = "LG TV",
- ModelNumber = "1.0",
- Identification = new()
- {
- FriendlyName = "My Device",
- Manufacturer = "LG Electronics",
- ManufacturerUrl = "http://www.lge.com",
- ModelDescription = "LG WebOSTV DMRplus",
- ModelName = "LG TV",
- ModelNumber = "1.0",
- }
- };
-
- var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification);
-
- Assert.False(deviceMatch);
- }
-
- [Fact]
- public void IsMatch_GivenNamesAndRegExMatch_ReturnsTrue()
- {
- var device = new DeviceInfo()
- {
- Name = "My Device"
- };
-
- var profile = new DeviceProfile()
- {
- Name = "Test Profile",
- FriendlyName = "My .*",
- Identification = new()
- };
-
- var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification);
-
- Assert.True(deviceMatch);
- }
- }
-}
diff --git a/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs b/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs
deleted file mode 100644
index 7655e3f7c..000000000
--- a/tests/Jellyfin.Dlna.Tests/GetUuidTests.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Emby.Dlna.PlayTo;
-using Xunit;
-
-namespace Jellyfin.Dlna.Tests
-{
- public static class GetUuidTests
- {
- [Theory]
- [InlineData("uuid:fc4ec57e-b051-11db-88f8-0060085db3f6::urn:schemas-upnp-org:device:WANDevice:1", "fc4ec57e-b051-11db-88f8-0060085db3f6")]
- [InlineData("uuid:IGD{8c80f73f-4ba0-45fa-835d-042505d052be}000000000000", "8c80f73f-4ba0-45fa-835d-042505d052be")]
- [InlineData("uuid:IGD{8c80f73f-4ba0-45fa-835d-042505d052be}000000000000::urn:schemas-upnp-org:device:InternetGatewayDevice:1", "8c80f73f-4ba0-45fa-835d-042505d052be")]
- [InlineData("uuid:00000000-0000-0000-0000-000000000000::upnp:rootdevice", "00000000-0000-0000-0000-000000000000")]
- [InlineData("uuid:fc4ec57e-b051-11db-88f8-0060085db3f6", "fc4ec57e-b051-11db-88f8-0060085db3f6")]
- public static void GetUuid_Valid_Success(string usn, string uuid)
- => Assert.Equal(uuid, PlayToManager.GetUuid(usn));
- }
-}
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
deleted file mode 100644
index 69677ce42..000000000
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ /dev/null
@@ -1,18 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" />
- <PackageReference Include="Moq" />
- <PackageReference Include="xunit" />
- <PackageReference Include="xunit.runner.visualstudio">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
- </PackageReference>
- <PackageReference Include="coverlet.collector" />
- </ItemGroup>
-
- <ItemGroup>
- <ProjectReference Include="../../Emby.Dlna/Emby.Dlna.csproj" />
- </ItemGroup>
-
-</Project>
diff --git a/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs
deleted file mode 100644
index c9018fe2f..000000000
--- a/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using Emby.Dlna.Server;
-using MediaBrowser.Model.Dlna;
-using Xunit;
-
-namespace Jellyfin.Dlna.Server.Tests;
-
-public class DescriptionXmlBuilderTests
-{
- [Fact]
- public void GetFriendlyName_EmptyProfile_ReturnsServerName()
- {
- const string ServerName = "Test Server Name";
- var builder = new DescriptionXmlBuilder(new DeviceProfile(), "serverUdn", "localhost", ServerName, string.Empty);
- Assert.Equal(ServerName, builder.GetFriendlyName());
- }
-
- [Fact]
- public void GetFriendlyName_FriendlyName_ReturnsFriendlyName()
- {
- const string FriendlyName = "Friendly Neighborhood Test Server";
- var builder = new DescriptionXmlBuilder(
- new DeviceProfile()
- {
- FriendlyName = FriendlyName
- },
- "serverUdn",
- "localhost",
- "Test Server Name",
- string.Empty);
- Assert.Equal(FriendlyName, builder.GetFriendlyName());
- }
-
- [Fact]
- public void GetFriendlyName_FriendlyNameInterpolation_ReturnsFriendlyName()
- {
- var builder = new DescriptionXmlBuilder(
- new DeviceProfile()
- {
- FriendlyName = "Friendly Neighborhood ${HostName}"
- },
- "serverUdn",
- "localhost",
- "Test Server Name",
- string.Empty);
- Assert.Equal("Friendly Neighborhood TestServerName", builder.GetFriendlyName());
- }
-}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
index f2ca2ff08..61105b42b 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
@@ -7,135 +7,128 @@ using Xunit;
namespace Jellyfin.Extensions.Tests.Json.Converters
{
- public static class JsonCommaDelimitedArrayTests
+ public class JsonCommaDelimitedArrayTests
{
+ private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
+ {
+ Converters =
+ {
+ new JsonStringEnumConverter()
+ }
+ };
+
[Fact]
- public static void Deserialize_String_Null_Success()
+ public void Deserialize_String_Null_Success()
{
- var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", _jsonOptions);
Assert.Null(value?.Value);
}
[Fact]
- public static void Deserialize_Empty_Success()
+ public void Deserialize_Empty_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
{
Value = Array.Empty<string>()
};
- var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": """" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": """" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_String_Valid_Success()
+ public void Deserialize_String_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
{
Value = new[] { "a", "b", "c" }
};
- var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_String_Space_Valid_Success()
+ public void Deserialize_String_Space_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
{
Value = new[] { "a", "b", "c" }
};
- var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_GenericCommandType_Valid_Success()
+ public void Deserialize_GenericCommandType_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_GenericCommandType_EmptyEntry_Success()
+ public void Deserialize_GenericCommandType_EmptyEntry_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_GenericCommandType_Invalid_Success()
+ public void Deserialize_GenericCommandType_Invalid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_GenericCommandType_Space_Valid_Success()
+ public void Deserialize_GenericCommandType_Space_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_String_Array_Valid_Success()
+ public void Deserialize_String_Array_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
{
Value = new[] { "a", "b", "c" }
};
- var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_GenericCommandType_Array_Valid_Success()
+ public void Deserialize_GenericCommandType_Array_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
index 92886dcd2..9b977b9a5 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
@@ -6,86 +6,85 @@ using Xunit;
namespace Jellyfin.Extensions.Tests.Json.Converters
{
- public static class JsonCommaDelimitedIReadOnlyListTests
+ public class JsonCommaDelimitedIReadOnlyListTests
{
+ private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
+ {
+ Converters =
+ {
+ new JsonStringEnumConverter()
+ }
+ };
+
[Fact]
- public static void Deserialize_String_Valid_Success()
+ public void Deserialize_String_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<string>
{
Value = new[] { "a", "b", "c" }
};
- var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_String_Space_Valid_Success()
+ public void Deserialize_String_Space_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<string>
{
Value = new[] { "a", "b", "c" }
};
- var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_GenericCommandType_Valid_Success()
+ public void Deserialize_GenericCommandType_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_GenericCommandType_Space_Valid_Success()
+ public void Deserialize_GenericCommandType_Space_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_String_Array_Valid_Success()
+ public void Deserialize_String_Array_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<string>
{
Value = new[] { "a", "b", "c" }
};
- var options = new JsonSerializerOptions();
- var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
- public static void Deserialize_GenericCommandType_Array_Valid_Success()
+ public void Deserialize_GenericCommandType_Array_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonStringEnumConverter());
- var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs
new file mode 100644
index 000000000..263f74c90
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.MediaEncoding.Encoder;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Tests.Probing
+{
+ public class ProbeExternalSourcesTests
+ {
+ [Fact]
+ public void GetExtraArguments_Forwards_UserAgent()
+ {
+ var encoder = new MediaEncoder(
+ Mock.Of<ILogger<MediaEncoder>>(),
+ Mock.Of<IServerConfigurationManager>(),
+ Mock.Of<IFileSystem>(),
+ Mock.Of<IBlurayExaminer>(),
+ Mock.Of<ILocalizationManager>(),
+ new ConfigurationBuilder().Build(),
+ Mock.Of<IServerConfigurationManager>());
+
+ var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";
+ var req = new MediaBrowser.Controller.MediaEncoding.MediaInfoRequest()
+ {
+ MediaSource = new MediaBrowser.Model.Dto.MediaSourceInfo
+ {
+ Path = "/path/to/stream",
+ Protocol = MediaProtocol.Http,
+ RequiredHttpHeaders = new Dictionary<string, string>()
+ {
+ { "user_agent", userAgent },
+ }
+ },
+ ExtractChapters = false,
+ MediaType = MediaBrowser.Model.Dlna.DlnaProfileType.Video,
+ };
+
+ var extraArg = encoder.GetExtraArguments(req);
+
+ Assert.Contains(userAgent, extraArg, StringComparison.InvariantCulture);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index c72a3315e..9b9c1ec34 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -14,18 +14,18 @@ namespace Jellyfin.Naming.Tests.AudioBook
data.Add(
new AudioBookFileInfo(
- @"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
+ "/server/AudioBooks/Larry Potter/Larry Potter.mp3",
"mp3"));
data.Add(
new AudioBookFileInfo(
- @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
+ "/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
"ogg",
chapterNumber: 1));
data.Add(
new AudioBookFileInfo(
- @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
+ "/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
"mp3",
chapterNumber: 2,
partNumber: 3));
@@ -49,7 +49,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
[Fact]
public void Resolve_InvalidExtension()
{
- var result = new AudioBookResolver(_namingOptions).Resolve(@"/server/AudioBooks/Larry Potter/Larry Potter.mp9");
+ var result = new AudioBookResolver(_namingOptions).Resolve("/server/AudioBooks/Larry Potter/Larry Potter.mp9");
Assert.Null(result);
}
diff --git a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
index 97949adff..ba602b5d2 100644
--- a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
@@ -20,11 +20,11 @@ public class ExternalPathParserTests
var hindiCultureDto = new CultureDto("Hindi", "Hindi", "hi", new[] { "hin" });
var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
- localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("en.*", RegexOptions.IgnoreCase)))
.Returns(englishCultureDto);
- localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"fr.*", RegexOptions.IgnoreCase)))
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("fr.*", RegexOptions.IgnoreCase)))
.Returns(frenchCultureDto);
- localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"hi.*", RegexOptions.IgnoreCase)))
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("hi.*", RegexOptions.IgnoreCase)))
.Returns(hindiCultureDto);
_audioPathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Audio);
diff --git a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
index c9a295a4c..471616797 100644
--- a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
@@ -12,34 +12,34 @@ namespace Jellyfin.Naming.Tests.Music
[InlineData("", false)]
[InlineData("C:/", false)]
[InlineData("/home/", false)]
- [InlineData(@"blah blah", false)]
- [InlineData(@"D:/music/weezer/03 Pinkerton", false)]
- [InlineData(@"D:/music/michael jackson/Bad (2012 Remaster)", false)]
- [InlineData(@"cd1", true)]
- [InlineData(@"disc18", true)]
- [InlineData(@"disk10", true)]
- [InlineData(@"vol7", true)]
- [InlineData(@"volume1", true)]
- [InlineData(@"cd 1", true)]
- [InlineData(@"disc 1", true)]
- [InlineData(@"disk 1", true)]
- [InlineData(@"disk", false)]
- [InlineData(@"disk ·", false)]
- [InlineData(@"disk a", false)]
- [InlineData(@"disk volume", false)]
- [InlineData(@"disc disc", false)]
- [InlineData(@"disk disc 6", false)]
- [InlineData(@"cd - 1", true)]
- [InlineData(@"disc- 1", true)]
- [InlineData(@"disk - 1", true)]
- [InlineData(@"Disc 01 (Hugo Wolf · 24 Lieder)", true)]
- [InlineData(@"Disc 04 (Encores and Folk Songs)", true)]
- [InlineData(@"Disc04 (Encores and Folk Songs)", true)]
- [InlineData(@"Disc 04(Encores and Folk Songs)", true)]
- [InlineData(@"Disc04(Encores and Folk Songs)", true)]
- [InlineData(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2", true)]
- [InlineData(@"[1985] Opportunities (Let's make lots of money) (1985)", false)]
- [InlineData(@"Blah 04(Encores and Folk Songs)", false)]
+ [InlineData("blah blah", false)]
+ [InlineData("D:/music/weezer/03 Pinkerton", false)]
+ [InlineData("D:/music/michael jackson/Bad (2012 Remaster)", false)]
+ [InlineData("cd1", true)]
+ [InlineData("disc18", true)]
+ [InlineData("disk10", true)]
+ [InlineData("vol7", true)]
+ [InlineData("volume1", true)]
+ [InlineData("cd 1", true)]
+ [InlineData("disc 1", true)]
+ [InlineData("disk 1", true)]
+ [InlineData("disk", false)]
+ [InlineData("disk ·", false)]
+ [InlineData("disk a", false)]
+ [InlineData("disk volume", false)]
+ [InlineData("disc disc", false)]
+ [InlineData("disk disc 6", false)]
+ [InlineData("cd - 1", true)]
+ [InlineData("disc- 1", true)]
+ [InlineData("disk - 1", true)]
+ [InlineData("Disc 01 (Hugo Wolf · 24 Lieder)", true)]
+ [InlineData("Disc 04 (Encores and Folk Songs)", true)]
+ [InlineData("Disc04 (Encores and Folk Songs)", true)]
+ [InlineData("Disc 04(Encores and Folk Songs)", true)]
+ [InlineData("Disc04(Encores and Folk Songs)", true)]
+ [InlineData("D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2", true)]
+ [InlineData("[1985] Opportunities (Let's make lots of money) (1985)", false)]
+ [InlineData("Blah 04(Encores and Folk Songs)", false)]
public void AlbumParser_MultidiscPath_Identifies(string path, bool result)
{
var parser = new AlbumParser(_namingOptions);
diff --git a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
index d0d3d8292..f2cd360e5 100644
--- a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
@@ -9,11 +9,11 @@ namespace Jellyfin.Naming.Tests.TV
private readonly EpisodeResolver _resolver = new EpisodeResolver(new NamingOptions());
[Theory]
- [InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)]
- [InlineData(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)]
- [InlineData(@"/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20)]
- [InlineData(@"/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24)]
- [InlineData(@"/server/Jeopardy 2023 07 14 HDTV x264 AC3.mkv", "Jeopardy", 2023, 07, 14)]
+ [InlineData("/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)]
+ [InlineData("/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)]
+ [InlineData("/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20)]
+ [InlineData("/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24)]
+ [InlineData("/server/Jeopardy 2023 07 14 HDTV x264 AC3.mkv", "Jeopardy", 2023, 07, 14)]
// TODO: [InlineData(@"/server/anything_14.11.1996.mp4", "anything", 1996, 11, 14)]
// TODO: [InlineData(@"/server/A Daily Show - (2015-01-15) - Episode Name - [720p].mkv", "A Daily Show", 2015, 01, 15)]
// TODO: [InlineData(@"/server/Last Man Standing_KTLADT_2018_05_25_01_28_00.wtv", "Last Man Standing", 2018, 05, 25)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
index 1da5a30a8..1727b2247 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
@@ -9,16 +9,16 @@ namespace Jellyfin.Naming.Tests.TV
private readonly EpisodeResolver _resolver = new EpisodeResolver(new NamingOptions());
[Theory]
- [InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")]
- [InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")]
- [InlineData(2, @"The Simpsons/02.avi")]
- [InlineData(2, @"The Simpsons/02 - Ep Name.avi")]
- [InlineData(2, @"The Simpsons/02-Ep Name.avi")]
- [InlineData(2, @"The Simpsons/02.EpName.avi")]
- [InlineData(2, @"The Simpsons/The Simpsons - 02.avi")]
- [InlineData(2, @"The Simpsons/The Simpsons - 02 Ep Name.avi")]
- [InlineData(7, @"GJ Club (2013)/GJ Club - 07.mkv")]
- [InlineData(17, @"Case Closed (1996-2007)/Case Closed - 317.mkv")]
+ [InlineData(8, "The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")]
+ [InlineData(2, "The Simpsons/The Simpsons - 02 - Ep Name.avi")]
+ [InlineData(2, "The Simpsons/02.avi")]
+ [InlineData(2, "The Simpsons/02 - Ep Name.avi")]
+ [InlineData(2, "The Simpsons/02-Ep Name.avi")]
+ [InlineData(2, "The Simpsons/02.EpName.avi")]
+ [InlineData(2, "The Simpsons/The Simpsons - 02.avi")]
+ [InlineData(2, "The Simpsons/The Simpsons - 02 Ep Name.avi")]
+ [InlineData(7, "GJ Club (2013)/GJ Club - 07.mkv")]
+ [InlineData(17, "Case Closed (1996-2007)/Case Closed - 317.mkv")]
// TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi")]
// TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 Ep Name.avi")]
// TODO: [InlineData(7, @"Seinfeld/Seinfeld 0807 The Checks.avi")]
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
index 7604ddc80..5397f1371 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
@@ -13,10 +13,10 @@ namespace Jellyfin.Naming.Tests.TV
[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(@"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)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
index ffaa64c3f..6d6591abf 100644
--- a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
@@ -9,66 +9,66 @@ namespace Jellyfin.Naming.Tests.TV
private readonly EpisodePathParser _episodePathParser = new EpisodePathParser(new NamingOptions());
[Theory]
- [InlineData(@"Season 1/4x01 – 20 Hours in America (1).mkv", null)]
- [InlineData(@"Season 1/01x02 blah.avi", null)]
- [InlineData(@"Season 1/S01x02 blah.avi", null)]
- [InlineData(@"Season 1/S01E02 blah.avi", null)]
- [InlineData(@"Season 1/S01xE02 blah.avi", null)]
- [InlineData(@"Season 1/seriesname 01x02 blah.avi", null)]
- [InlineData(@"Season 1/seriesname S01x02 blah.avi", null)]
- [InlineData(@"Season 1/seriesname S01E02 blah.avi", null)]
- [InlineData(@"Season 1/seriesname S01xE02 blah.avi", null)]
- [InlineData(@"Season 2/02x03 - 04 Ep Name.mp4", null)]
- [InlineData(@"Season 2/My show name 02x03 - 04 Ep Name.mp4", null)]
- [InlineData(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2/02x03-04-15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/02x03-E15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/02x03x04x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 26)]
- [InlineData(@"Season 1/S01E23-E24-E26 - The Woman.mp4", 26)]
+ [InlineData("Season 1/4x01 – 20 Hours in America (1).mkv", null)]
+ [InlineData("Season 1/01x02 blah.avi", null)]
+ [InlineData("Season 1/S01x02 blah.avi", null)]
+ [InlineData("Season 1/S01E02 blah.avi", null)]
+ [InlineData("Season 1/S01xE02 blah.avi", null)]
+ [InlineData("Season 1/seriesname 01x02 blah.avi", null)]
+ [InlineData("Season 1/seriesname S01x02 blah.avi", null)]
+ [InlineData("Season 1/seriesname S01E02 blah.avi", null)]
+ [InlineData("Season 1/seriesname S01xE02 blah.avi", null)]
+ [InlineData("Season 2/02x03 - 04 Ep Name.mp4", null)]
+ [InlineData("Season 2/My show name 02x03 - 04 Ep Name.mp4", null)]
+ [InlineData("Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2/02x03-04-15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/02x03-E15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/02x03 - x04 - x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 26)]
+ [InlineData("Season 1/S01E23-E24-E26 - The Woman.mp4", 26)]
// Four Digits seasons
- [InlineData(@"Season 2009/2009x02 blah.avi", null)]
- [InlineData(@"Season 2009/S2009x02 blah.avi", null)]
- [InlineData(@"Season 2009/S2009E02 blah.avi", null)]
- [InlineData(@"Season 2009/S2009xE02 blah.avi", null)]
- [InlineData(@"Season 2009/seriesname 2009x02 blah.avi", null)]
- [InlineData(@"Season 2009/seriesname S2009x02 blah.avi", null)]
- [InlineData(@"Season 2009/seriesname S2009E02 blah.avi", null)]
- [InlineData(@"Season 2009/seriesname S2009xE02 blah.avi", null)]
- [InlineData(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03-04-15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03-E15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03x04x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 26)]
- [InlineData(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4", 26)]
+ [InlineData("Season 2009/2009x02 blah.avi", null)]
+ [InlineData("Season 2009/S2009x02 blah.avi", null)]
+ [InlineData("Season 2009/S2009E02 blah.avi", null)]
+ [InlineData("Season 2009/S2009xE02 blah.avi", null)]
+ [InlineData("Season 2009/seriesname 2009x02 blah.avi", null)]
+ [InlineData("Season 2009/seriesname S2009x02 blah.avi", null)]
+ [InlineData("Season 2009/seriesname S2009E02 blah.avi", null)]
+ [InlineData("Season 2009/seriesname S2009xE02 blah.avi", null)]
+ [InlineData("Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03-04-15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03-E15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03x04x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 26)]
+ [InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 26)]
// Without season number
- [InlineData(@"Season 1/02 - blah.avi", null)]
- [InlineData(@"Season 2/02 - blah 14 blah.avi", null)]
- [InlineData(@"Season 1/02 - blah-02 a.avi", null)]
- [InlineData(@"Season 2/02.avi", null)]
- [InlineData(@"Season 1/02-03 - blah.avi", 3)]
- [InlineData(@"Season 2/02-04 - blah 14 blah.avi", 4)]
- [InlineData(@"Season 1/02-05 - blah-02 a.avi", 5)]
- [InlineData(@"Season 2/02-04.avi", 4)]
- [InlineData(@"Season 2 /[HorribleSubs] Hunter X Hunter - 136[720p].mkv", null)]
+ [InlineData("Season 1/02 - blah.avi", null)]
+ [InlineData("Season 2/02 - blah 14 blah.avi", null)]
+ [InlineData("Season 1/02 - blah-02 a.avi", null)]
+ [InlineData("Season 2/02.avi", null)]
+ [InlineData("Season 1/02-03 - blah.avi", 3)]
+ [InlineData("Season 2/02-04 - blah 14 blah.avi", 4)]
+ [InlineData("Season 1/02-05 - blah-02 a.avi", 5)]
+ [InlineData("Season 2/02-04.avi", 4)]
+ [InlineData("Season 2 /[HorribleSubs] Hunter X Hunter - 136[720p].mkv", null)]
// With format specification that must not be detected as ending episode number
- [InlineData(@"Season 1/series-s09e14-1080p.mkv", null)]
- [InlineData(@"Season 1/series-s09e14-720p.mkv", null)]
- [InlineData(@"Season 1/series-s09e14-720i.mkv", null)]
- [InlineData(@"Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)]
- [InlineData(@"Season 1/MOONLIGHTING_s01e01-e04", 4)]
+ [InlineData("Season 1/series-s09e14-1080p.mkv", null)]
+ [InlineData("Season 1/series-s09e14-720p.mkv", null)]
+ [InlineData("Season 1/series-s09e14-720i.mkv", null)]
+ [InlineData("Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)]
+ [InlineData("Season 1/MOONLIGHTING_s01e01-e04", 4)]
public void TestGetEndingEpisodeNumberFromFile(string filename, int? endingEpisodeNumber)
{
var result = _episodePathParser.Parse(filename, false);
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
index 55af33836..6773bbeb1 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
@@ -6,23 +6,23 @@ namespace Jellyfin.Naming.Tests.TV
public class SeasonFolderTests
{
[Theory]
- [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)]
+ [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);
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
index 58ec1b5d2..94a953de3 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
@@ -51,8 +51,8 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 2009)]
[InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 2009)]
[InlineData("Series/1-12 - The Woman.mp4", 1)]
- [InlineData(@"Running Man/Running Man S2017E368.mkv", 2017)]
- [InlineData(@"Case Closed (1996-2007)/Case Closed - 317.mkv", 3)]
+ [InlineData("Running Man/Running Man S2017E368.mkv", 2017)]
+ [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 3)]
// TODO: [InlineData(@"Seinfeld/Seinfeld 0807 The Checks.avi", 8)]
public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected)
{
diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
index fa46ecc3a..3721cd28c 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
@@ -21,8 +21,8 @@ namespace Jellyfin.Naming.Tests.TV
[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)]
- [InlineData(@"/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)]
- [InlineData(@"Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)]
+ [InlineData("/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)]
+ [InlineData("Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)]
[InlineData("[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken/[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken - 12 (NVENC H.265 1080p).mkv", "Tensura Nikki - Tensei Shitara Slime Datta Ken", null, 12)]
[InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
[InlineData("Series/4-12 - The Woman.mp4", "", 4, 12, 12)]
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
index b1141df47..62d60e5a4 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
@@ -10,34 +10,34 @@ namespace Jellyfin.Naming.Tests.Video
private readonly NamingOptions _namingOptions = new NamingOptions();
[Theory]
- [InlineData(@"The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013)]
- [InlineData(@"The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013)]
- [InlineData(@"The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013)]
- [InlineData(@"The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013)]
- [InlineData(@"300 (2006).mkv", "300", 2006)]
- [InlineData(@"d:/movies/300 (2006).mkv", "300", 2006)]
- [InlineData(@"300 2 (2006).mkv", "300 2", 2006)]
- [InlineData(@"300 - 2 (2006).mkv", "300 - 2", 2006)]
- [InlineData(@"300 2001 (2006).mkv", "300 2001", 2006)]
- [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013)]
- [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013)]
- [InlineData(@"/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006)]
- [InlineData(@"Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016)]
- [InlineData(@"The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013)]
- [InlineData(@"The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013)]
- [InlineData(@"The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013)]
- [InlineData(@"The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013)]
- [InlineData(@"300 (2006)", "300", 2006)]
- [InlineData(@"d:/movies/300 (2006)", "300", 2006)]
- [InlineData(@"300 2 (2006)", "300 2", 2006)]
- [InlineData(@"300 - 2 (2006)", "300 - 2", 2006)]
- [InlineData(@"300 2001 (2006)", "300 2001", 2006)]
- [InlineData(@"/server/Movies/300 (2007)/300 (2006)", "300", 2006)]
- [InlineData(@"/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006)]
- [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv", null)]
- [InlineData(@"American Psycho.mkv", "American Psycho.mkv", null)]
- [InlineData(@"[rec].mkv", "[rec].mkv", null)]
- [InlineData(@"St. Vincent (2014)", "St. Vincent", 2014)]
+ [InlineData("The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013)]
+ [InlineData("The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013)]
+ [InlineData("The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013)]
+ [InlineData("The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013)]
+ [InlineData("300 (2006).mkv", "300", 2006)]
+ [InlineData("d:/movies/300 (2006).mkv", "300", 2006)]
+ [InlineData("300 2 (2006).mkv", "300 2", 2006)]
+ [InlineData("300 - 2 (2006).mkv", "300 - 2", 2006)]
+ [InlineData("300 2001 (2006).mkv", "300 2001", 2006)]
+ [InlineData("curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013)]
+ [InlineData("curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013)]
+ [InlineData("/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006)]
+ [InlineData("Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016)]
+ [InlineData("The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013)]
+ [InlineData("The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013)]
+ [InlineData("The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013)]
+ [InlineData("The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013)]
+ [InlineData("300 (2006)", "300", 2006)]
+ [InlineData("d:/movies/300 (2006)", "300", 2006)]
+ [InlineData("300 2 (2006)", "300 2", 2006)]
+ [InlineData("300 - 2 (2006)", "300 - 2", 2006)]
+ [InlineData("300 2001 (2006)", "300 2001", 2006)]
+ [InlineData("/server/Movies/300 (2007)/300 (2006)", "300", 2006)]
+ [InlineData("/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006)]
+ [InlineData("American.Psycho.mkv", "American.Psycho.mkv", null)]
+ [InlineData("American Psycho.mkv", "American Psycho.mkv", null)]
+ [InlineData("[rec].mkv", "[rec].mkv", null)]
+ [InlineData("St. Vincent (2014)", "St. Vincent", 2014)]
[InlineData("Super movie(2009).mp4", "Super movie", 2009)]
[InlineData("Drug War 2013.mp4", "Drug War", 2013)]
[InlineData("My Movie (1997) - GreatestReleaseGroup 2019.mp4", "My Movie", 1997)]
@@ -45,9 +45,9 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("First Man (2018) 1080p.mkv", "First Man", 2018)]
[InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)]
// FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)]
- [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+ [InlineData("3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
[InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)]
- [InlineData(@"Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)]
+ [InlineData("Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)]
[InlineData("My Movie 2013.12.09", "My Movie 2013.12.09", null)]
[InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)]
[InlineData("My Movie 20131209", "My Movie 20131209", null)]
diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
index 511a014a6..fccce5bff 100644
--- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
@@ -22,7 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void Test3DName()
{
- var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions);
+ var result = VideoResolver.ResolveFile("C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions);
Assert.Equal("hsbs", result?.Format3D);
Assert.Equal("Oblivion", result?.Name);
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 294f11ee7..183ec8984 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -15,10 +15,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
};
var result = VideoListResolver.Resolve(
@@ -34,10 +34,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
};
var result = VideoListResolver.Resolve(
@@ -54,8 +54,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv",
- @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
+ "/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv",
+ "/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
};
var result = VideoListResolver.Resolve(
@@ -71,13 +71,13 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/M/Movie 1.mkv",
- @"/movies/M/Movie 2.mkv",
- @"/movies/M/Movie 3.mkv",
- @"/movies/M/Movie 4.mkv",
- @"/movies/M/Movie 5.mkv",
- @"/movies/M/Movie 6.mkv",
- @"/movies/M/Movie 7.mkv"
+ "/movies/M/Movie 1.mkv",
+ "/movies/M/Movie 2.mkv",
+ "/movies/M/Movie 3.mkv",
+ "/movies/M/Movie 4.mkv",
+ "/movies/M/Movie 5.mkv",
+ "/movies/M/Movie 6.mkv",
+ "/movies/M/Movie 7.mkv"
};
var result = VideoListResolver.Resolve(
@@ -93,14 +93,14 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Movie/Movie.mkv",
- @"/movies/Movie/Movie-2.mkv",
- @"/movies/Movie/Movie-3.mkv",
- @"/movies/Movie/Movie-4.mkv",
- @"/movies/Movie/Movie-5.mkv",
- @"/movies/Movie/Movie-6.mkv",
- @"/movies/Movie/Movie-7.mkv",
- @"/movies/Movie/Movie-8.mkv"
+ "/movies/Movie/Movie.mkv",
+ "/movies/Movie/Movie-2.mkv",
+ "/movies/Movie/Movie-3.mkv",
+ "/movies/Movie/Movie-4.mkv",
+ "/movies/Movie/Movie-5.mkv",
+ "/movies/Movie/Movie-6.mkv",
+ "/movies/Movie/Movie-7.mkv",
+ "/movies/Movie/Movie-8.mkv"
};
var result = VideoListResolver.Resolve(
@@ -116,15 +116,15 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Mo/Movie 1.mkv",
- @"/movies/Mo/Movie 2.mkv",
- @"/movies/Mo/Movie 3.mkv",
- @"/movies/Mo/Movie 4.mkv",
- @"/movies/Mo/Movie 5.mkv",
- @"/movies/Mo/Movie 6.mkv",
- @"/movies/Mo/Movie 7.mkv",
- @"/movies/Mo/Movie 8.mkv",
- @"/movies/Mo/Movie 9.mkv"
+ "/movies/Mo/Movie 1.mkv",
+ "/movies/Mo/Movie 2.mkv",
+ "/movies/Mo/Movie 3.mkv",
+ "/movies/Mo/Movie 4.mkv",
+ "/movies/Mo/Movie 5.mkv",
+ "/movies/Mo/Movie 6.mkv",
+ "/movies/Mo/Movie 7.mkv",
+ "/movies/Mo/Movie 8.mkv",
+ "/movies/Mo/Movie 9.mkv"
};
var result = VideoListResolver.Resolve(
@@ -140,11 +140,11 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Movie/Movie 1.mkv",
- @"/movies/Movie/Movie 2.mkv",
- @"/movies/Movie/Movie 3.mkv",
- @"/movies/Movie/Movie 4.mkv",
- @"/movies/Movie/Movie 5.mkv"
+ "/movies/Movie/Movie 1.mkv",
+ "/movies/Movie/Movie 2.mkv",
+ "/movies/Movie/Movie 3.mkv",
+ "/movies/Movie/Movie 4.mkv",
+ "/movies/Movie/Movie 5.mkv"
};
var result = VideoListResolver.Resolve(
@@ -162,11 +162,11 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
- @"/movies/Iron Man/Iron Man.mkv",
- @"/movies/Iron Man/Iron Man (2008).mkv",
- @"/movies/Iron Man/Iron Man (2009).mkv",
- @"/movies/Iron Man/Iron Man (2010).mkv",
- @"/movies/Iron Man/Iron Man (2011).mkv"
+ "/movies/Iron Man/Iron Man.mkv",
+ "/movies/Iron Man/Iron Man (2008).mkv",
+ "/movies/Iron Man/Iron Man (2009).mkv",
+ "/movies/Iron Man/Iron Man (2010).mkv",
+ "/movies/Iron Man/Iron Man (2011).mkv"
};
var result = VideoListResolver.Resolve(
@@ -182,13 +182,13 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Iron Man/Iron Man.mkv",
- @"/movies/Iron Man/Iron Man-720p.mkv",
- @"/movies/Iron Man/Iron Man-test.mkv",
- @"/movies/Iron Man/Iron Man-bluray.mkv",
- @"/movies/Iron Man/Iron Man-3d.mkv",
- @"/movies/Iron Man/Iron Man-3d-hsbs.mkv",
- @"/movies/Iron Man/Iron Man[test].mkv"
+ "/movies/Iron Man/Iron Man.mkv",
+ "/movies/Iron Man/Iron Man-720p.mkv",
+ "/movies/Iron Man/Iron Man-test.mkv",
+ "/movies/Iron Man/Iron Man-bluray.mkv",
+ "/movies/Iron Man/Iron Man-3d.mkv",
+ "/movies/Iron Man/Iron Man-3d-hsbs.mkv",
+ "/movies/Iron Man/Iron Man[test].mkv"
};
var result = VideoListResolver.Resolve(
@@ -211,13 +211,13 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Iron Man/Iron Man.mkv",
- @"/movies/Iron Man/Iron Man - 720p.mkv",
- @"/movies/Iron Man/Iron Man - test.mkv",
- @"/movies/Iron Man/Iron Man - bluray.mkv",
- @"/movies/Iron Man/Iron Man - 3d.mkv",
- @"/movies/Iron Man/Iron Man - 3d-hsbs.mkv",
- @"/movies/Iron Man/Iron Man [test].mkv"
+ "/movies/Iron Man/Iron Man.mkv",
+ "/movies/Iron Man/Iron Man - 720p.mkv",
+ "/movies/Iron Man/Iron Man - test.mkv",
+ "/movies/Iron Man/Iron Man - bluray.mkv",
+ "/movies/Iron Man/Iron Man - 3d.mkv",
+ "/movies/Iron Man/Iron Man - 3d-hsbs.mkv",
+ "/movies/Iron Man/Iron Man [test].mkv"
};
var result = VideoListResolver.Resolve(
@@ -240,8 +240,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Iron Man/Iron Man - B (2006).mkv",
- @"/movies/Iron Man/Iron Man - C (2007).mkv"
+ "/movies/Iron Man/Iron Man - B (2006).mkv",
+ "/movies/Iron Man/Iron Man - C (2007).mkv"
};
var result = VideoListResolver.Resolve(
@@ -256,13 +256,13 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Iron Man/Iron Man.mkv",
- @"/movies/Iron Man/Iron Man_720p.mkv",
- @"/movies/Iron Man/Iron Man_test.mkv",
- @"/movies/Iron Man/Iron Man_bluray.mkv",
- @"/movies/Iron Man/Iron Man_3d.mkv",
- @"/movies/Iron Man/Iron Man_3d-hsbs.mkv",
- @"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
+ "/movies/Iron Man/Iron Man.mkv",
+ "/movies/Iron Man/Iron Man_720p.mkv",
+ "/movies/Iron Man/Iron Man_test.mkv",
+ "/movies/Iron Man/Iron Man_bluray.mkv",
+ "/movies/Iron Man/Iron Man_3d.mkv",
+ "/movies/Iron Man/Iron Man_3d-hsbs.mkv",
+ "/movies/Iron Man/Iron Man_3d.hsbs.mkv"
};
var result = VideoListResolver.Resolve(
@@ -280,11 +280,11 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
- @"/movies/Iron Man/Iron Man (2007).mkv",
- @"/movies/Iron Man/Iron Man (2008).mkv",
- @"/movies/Iron Man/Iron Man (2009).mkv",
- @"/movies/Iron Man/Iron Man (2010).mkv",
- @"/movies/Iron Man/Iron Man (2011).mkv"
+ "/movies/Iron Man/Iron Man (2007).mkv",
+ "/movies/Iron Man/Iron Man (2008).mkv",
+ "/movies/Iron Man/Iron Man (2009).mkv",
+ "/movies/Iron Man/Iron Man (2010).mkv",
+ "/movies/Iron Man/Iron Man (2011).mkv"
};
var result = VideoListResolver.Resolve(
@@ -300,8 +300,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv",
- @"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
+ "/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv",
+ "/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
};
var result = VideoListResolver.Resolve(
@@ -317,8 +317,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
};
var result = VideoListResolver.Resolve(
@@ -334,12 +334,12 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
};
var result = VideoListResolver.Resolve(
@@ -361,8 +361,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv",
- @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
+ "/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv",
+ "/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
};
var result = VideoListResolver.Resolve(
@@ -378,8 +378,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv",
- @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
+ "/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv",
+ "/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
};
var result = VideoListResolver.Resolve(
diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
index 97b52f749..c95703f53 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
@@ -384,8 +384,8 @@ namespace Jellyfin.Naming.Tests.Video
// No stacking here because there is no part/disc/etc
var files = new[]
{
- @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)",
- @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
+ "M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)",
+ "M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
};
var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
index 1d50df7a6..fc852ae85 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
@@ -29,7 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestStubName()
{
- var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions);
+ var result = VideoResolver.ResolveFile("C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions);
Assert.Equal("Oblivion", result?.Name);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 0316377d4..377f82eac 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -200,8 +200,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1",
- @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
+ "M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1",
+ "M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
};
var result = VideoListResolver.Resolve(
@@ -217,8 +217,8 @@ namespace Jellyfin.Naming.Tests.Video
// These should be considered separate, unrelated videos
var files = new[]
{
- @"My movie #1.mp4",
- @"My movie #2.mp4"
+ "My movie #1.mp4",
+ "My movie #2.mp4"
};
var result = VideoListResolver.Resolve(
@@ -233,10 +233,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"No (2012) part1.mp4",
- @"No (2012) part2.mp4",
- @"No (2012) part1-trailer.mp4",
- @"No (2012)-trailer.mp4"
+ "No (2012) part1.mp4",
+ "No (2012) part2.mp4",
+ "No (2012) part1-trailer.mp4",
+ "No (2012)-trailer.mp4"
};
var result = VideoListResolver.Resolve(
@@ -254,10 +254,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/Movies/Top Gun (1984)/movie.mp4",
- @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
- @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
- @"/Movies/trailer.mp4"
+ "/Movies/Top Gun (1984)/movie.mp4",
+ "/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
+ "/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
+ "/Movies/trailer.mp4"
};
var result = VideoListResolver.Resolve(
@@ -276,10 +276,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi",
- @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi",
- @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi",
- @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
+ "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi",
+ "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi",
+ "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi",
+ "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
};
var result = VideoListResolver.Resolve(
@@ -294,7 +294,7 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
+ "/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
};
var result = VideoListResolver.Resolve(
@@ -309,7 +309,7 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"The Colony.mkv"
+ "The Colony.mkv"
};
var result = VideoListResolver.Resolve(
@@ -324,8 +324,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"Four Sisters and a Wedding - A.avi",
- @"Four Sisters and a Wedding - B.avi"
+ "Four Sisters and a Wedding - A.avi",
+ "Four Sisters and a Wedding - B.avi"
};
var result = VideoListResolver.Resolve(
@@ -342,8 +342,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"Four Rooms - A.avi",
- @"Four Rooms - A.mp4"
+ "Four Rooms - A.avi",
+ "Four Rooms - A.mp4"
};
var result = VideoListResolver.Resolve(
@@ -358,8 +358,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/Server/Despicable Me/Despicable Me (2010).mkv",
- @"/Server/Despicable Me/trailer.mkv"
+ "/Server/Despicable Me/Despicable Me (2010).mkv",
+ "/Server/Despicable Me/trailer.mkv"
};
var result = VideoListResolver.Resolve(
@@ -376,8 +376,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/Server/Despicable Me/Despicable Me (2010).mkv",
- @"/Server/Despicable Me/trailers/some title.mkv"
+ "/Server/Despicable Me/Despicable Me (2010).mkv",
+ "/Server/Despicable Me/trailers/some title.mkv"
};
var result = VideoListResolver.Resolve(
@@ -394,8 +394,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/Movies/Despicable Me/Despicable Me.mkv",
- @"/Movies/Despicable Me/trailers/trailer.mkv"
+ "/Movies/Despicable Me/Despicable Me.mkv",
+ "/Movies/Despicable Me/trailers/trailer.mkv"
};
var result = VideoListResolver.Resolve(
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index 33a99e107..8455a56a1 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -15,26 +15,26 @@ namespace Jellyfin.Naming.Tests.Video
var data = new TheoryData<VideoFileInfo>();
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
+ path: "/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
container: "mkv",
name: "7 Psychos"));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
+ path: "/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
container: "mkv",
name: "3 days to kill",
year: 2005));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/American Psycho/American.Psycho.mkv",
+ path: "/server/Movies/American Psycho/American.Psycho.mkv",
container: "mkv",
name: "American.Psycho"));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
+ path: "/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
container: "mkv",
name: "brave",
year: 2006,
@@ -43,14 +43,14 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
+ path: "/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
container: "mkv",
name: "300",
year: 2006));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
+ path: "/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
container: "mkv",
name: "300",
year: 2006,
@@ -59,7 +59,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
+ path: "/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
container: "disc",
name: "brave",
year: 2006,
@@ -68,7 +68,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
+ path: "/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
container: "disc",
name: "300",
year: 2006,
@@ -77,7 +77,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
+ path: "/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
container: "disc",
name: "Brave",
year: 2006,
@@ -86,7 +86,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
+ path: "/server/Movies/300 (2007)/300 (2006).bluray.disc",
container: "disc",
name: "300",
year: 2006,
@@ -95,7 +95,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
+ path: "/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
container: "mkv",
name: "300",
year: 2006,
@@ -103,7 +103,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
+ path: "/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
container: "mkv",
name: "Brave",
year: 2006,
@@ -111,28 +111,28 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006).mkv",
+ path: "/server/Movies/300 (2007)/300 (2006).mkv",
container: "mkv",
name: "300",
year: 2006));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
+ path: "/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
container: "mkv",
name: "Bad Boys",
year: 1995));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Brave (2007)/Brave (2006).mkv",
+ path: "/server/Movies/Brave (2007)/Brave (2006).mkv",
container: "mkv",
name: "Brave",
year: 2006));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4",
+ path: "/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4",
container: "mp4",
name: "Rain Man",
year: 1988));
@@ -174,8 +174,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var paths = new[]
{
- @"/Server/Iron Man",
- @"Batman",
+ "/Server/Iron Man",
+ "Batman",
string.Empty
};
diff --git a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs
index a78b872df..30726f1d3 100644
--- a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs
+++ b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Net;
using Xunit;
namespace Jellyfin.Networking.Tests.Configuration;
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 3747db3bb..2d7f11210 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -18,7 +18,7 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="../../Jellyfin.Networking/Jellyfin.Networking.csproj" />
+ <ProjectReference Include="../../src/Jellyfin.Networking/Jellyfin.Networking.csproj" />
</ItemGroup>
</Project>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
index 072e0a8c5..01546aa2b 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
@@ -1,6 +1,6 @@
using FsCheck;
using FsCheck.Xunit;
-using Jellyfin.Networking.Extensions;
+using MediaBrowser.Common.Net;
using Xunit;
namespace Jellyfin.Networking.Tests
@@ -26,15 +26,15 @@ namespace Jellyfin.Networking.Tests
[InlineData("192.168.1.2/255.255.255.0")]
[InlineData("192.168.1.2/24")]
public static void TryParse_ValidHostStrings_True(string address)
- => Assert.True(NetworkExtensions.TryParseHost(address, out _, true, true));
+ => Assert.True(NetworkUtils.TryParseHost(address, out _, true, true));
[Property]
public static Property TryParse_IPv4Address_True(IPv4Address address)
- => NetworkExtensions.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty();
+ => NetworkUtils.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty();
[Property]
public static Property TryParse_IPv6Address_True(IPv6Address address)
- => NetworkExtensions.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty();
+ => NetworkUtils.TryParseHost(address.Item.ToString(), out _, true, true).ToProperty();
/// <summary>
/// All should be invalid address strings.
@@ -47,6 +47,6 @@ namespace Jellyfin.Networking.Tests
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
public static void TryParse_InvalidAddressString_False(string address)
- => Assert.False(NetworkExtensions.TryParseHost(address, out _, true, true));
+ => Assert.False(NetworkUtils.TryParseHost(address, out _, true, true));
}
}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
index 2302f90b8..0333d98e6 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
@@ -1,6 +1,6 @@
using System.Net;
-using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
+using MediaBrowser.Common.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 022b8a3d0..3b7c43100 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -2,15 +2,15 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
-using Jellyfin.Networking.Configuration;
-using Jellyfin.Networking.Extensions;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
namespace Jellyfin.Networking.Tests
{
@@ -71,7 +71,6 @@ namespace Jellyfin.Networking.Tests
[InlineData("127.0.0.1/8")]
[InlineData("192.168.1.2")]
[InlineData("192.168.1.2/24")]
- [InlineData("192.168.1.2/255.255.255.0")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")]
[InlineData("fe80::7add:12ff:febb:c67b%16")]
@@ -80,7 +79,7 @@ namespace Jellyfin.Networking.Tests
[InlineData("[fe80::7add:12ff:febb:c67b%16]")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
public static void TryParseValidIPStringsTrue(string address)
- => Assert.True(NetworkExtensions.TryParseToSubnet(address, out _));
+ => Assert.True(NetworkUtils.TryParseToSubnet(address, out _));
/// <summary>
/// Checks invalid IP address formats.
@@ -93,7 +92,7 @@ namespace Jellyfin.Networking.Tests
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
public static void TryParseInvalidIPStringsFalse(string address)
- => Assert.False(NetworkExtensions.TryParseToSubnet(address, out _));
+ => Assert.False(NetworkUtils.TryParseToSubnet(address, out _));
/// <summary>
/// Checks if IPv4 address is within a defined subnet.
@@ -103,17 +102,15 @@ namespace Jellyfin.Networking.Tests
[Theory]
[InlineData("192.168.5.85/24", "192.168.5.1")]
[InlineData("192.168.5.85/24", "192.168.5.254")]
- [InlineData("192.168.5.85/255.255.255.0", "192.168.5.254")]
[InlineData("10.128.240.50/30", "10.128.240.48")]
[InlineData("10.128.240.50/30", "10.128.240.49")]
[InlineData("10.128.240.50/30", "10.128.240.50")]
[InlineData("10.128.240.50/30", "10.128.240.51")]
- [InlineData("10.128.240.50/255.255.255.252", "10.128.240.51")]
[InlineData("127.0.0.1/8", "127.0.0.1")]
public void IPv4SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress)
{
var ipa = IPAddress.Parse(ipAddress);
- Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
+ Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
}
/// <summary>
@@ -124,16 +121,14 @@ namespace Jellyfin.Networking.Tests
[Theory]
[InlineData("192.168.5.85/24", "192.168.4.254")]
[InlineData("192.168.5.85/24", "191.168.5.254")]
- [InlineData("192.168.5.85/255.255.255.252", "192.168.4.254")]
[InlineData("10.128.240.50/30", "10.128.240.47")]
[InlineData("10.128.240.50/30", "10.128.240.52")]
[InlineData("10.128.240.50/30", "10.128.239.50")]
[InlineData("10.128.240.50/30", "10.127.240.51")]
- [InlineData("10.128.240.50/255.255.255.252", "10.127.240.51")]
public void IPv4SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress)
{
var ipa = IPAddress.Parse(ipAddress);
- Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
+ Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
}
/// <summary>
@@ -149,7 +144,7 @@ namespace Jellyfin.Networking.Tests
[InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")]
public void IPv6SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress)
{
- Assert.True(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
+ Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
}
[Theory]
@@ -160,7 +155,7 @@ namespace Jellyfin.Networking.Tests
[InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")]
public void IPv6SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress)
{
- Assert.False(NetworkExtensions.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
+ Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
}
[Theory]
@@ -207,7 +202,7 @@ namespace Jellyfin.Networking.Tests
NetworkManager.MockNetworkSettings = string.Empty;
// Check to see if DNS resolution is working. If not, skip test.
- if (!NetworkExtensions.TryParseHost(source, out var host))
+ if (!NetworkUtils.TryParseHost(source, out var host))
{
return;
}
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
index f157f01e5..be5a401b1 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -27,7 +27,7 @@ namespace Jellyfin.Providers.Tests.Manager
{
public partial class ItemImageProviderTests
{
- private const string TestDataImagePath = "Test Data/Images/blank{0}.jpg";
+ private static readonly CompositeFormat _testDataImagePath = CompositeFormat.Parse("Test Data/Images/blank{0}.jpg");
[GeneratedRegex("[0-9]+")]
private static partial Regex NumbersRegex();
@@ -275,7 +275,7 @@ namespace Jellyfin.Providers.Tests.Manager
{
HasImage = true,
Format = ImageFormat.Jpg,
- Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0) : null,
+ Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 0) : null,
Protocol = protocol
};
@@ -352,11 +352,11 @@ namespace Jellyfin.Providers.Tests.Manager
{
if (forceRefresh)
{
- Assert.Matches(@"image url [0-9]", image.Path);
+ Assert.Matches("image url [0-9]", image.Path);
}
else
{
- Assert.DoesNotMatch(@"image url [0-9]", image.Path);
+ Assert.DoesNotMatch("image url [0-9]", image.Path);
}
}
}
@@ -563,21 +563,21 @@ namespace Jellyfin.Providers.Tests.Manager
mockFileSystem.Setup(fs => fs.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(new[]
{
- string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0),
- string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 1)
+ string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 0),
+ string.Format(CultureInfo.InvariantCulture, _testDataImagePath, 1)
});
return new ItemImageProvider(new NullLogger<ItemImageProvider>(), providerManager, mockFileSystem.Object);
}
- private static BaseItem GetItemWithImages(ImageType type, int count, bool validPaths)
+ private static Video GetItemWithImages(ImageType type, int count, bool validPaths)
{
// Has to exist for querying DateModified time on file, results stored but not checked so not populating
BaseItem.FileSystem ??= Mock.Of<IFileSystem>();
var item = new Video();
- var path = validPaths ? TestDataImagePath : "invalid path {0}";
+ var path = validPaths ? _testDataImagePath.Format : "invalid path {0}";
for (int i = 0; i < count; i++)
{
item.SetImagePath(type, i, new FileSystemMetadata
@@ -604,7 +604,7 @@ namespace Jellyfin.Providers.Tests.Manager
/// </summary>
private static LocalImageInfo[] GetImages(ImageType type, int count, bool validPaths)
{
- var path = validPaths ? TestDataImagePath : "invalid path {0}";
+ var path = validPaths ? _testDataImagePath.Format : "invalid path {0}";
var images = new LocalImageInfo[count];
for (int i = 0; i < count; i++)
{
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
index 6ee4b8ef2..2b3867512 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
@@ -29,7 +29,7 @@ public class MediaInfoResolverTests
public const string VideoDirectoryPath = "Test Data/Video";
public const string VideoDirectoryRegex = @"Test Data[/\\]Video";
public const string MetadataDirectoryPath = "library/00/00000000000000000000000000000000";
- public const string MetadataDirectoryRegex = @"library.*";
+ public const string MetadataDirectoryRegex = "library.*";
private readonly ILocalizationManager _localizationManager;
private readonly MediaInfoResolver _subtitleResolver;
@@ -49,7 +49,7 @@ public class MediaInfoResolverTests
var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" });
var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
- localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("en.*", RegexOptions.IgnoreCase)))
.Returns(englishCultureDto);
_localizationManager = localizationManager.Object;
@@ -79,7 +79,7 @@ public class MediaInfoResolverTests
{
// need a media source manager capable of returning something other than file protocol
var mediaSourceManager = new Mock<IMediaSourceManager>();
- mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*")))
+ mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex("http.*")))
.Returns(MediaProtocol.Http);
BaseItem.MediaSourceManager = mediaSourceManager.Object;
@@ -186,7 +186,7 @@ public class MediaInfoResolverTests
{
// need a media source manager capable of returning something other than file protocol
var mediaSourceManager = new Mock<IMediaSourceManager>();
- mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*")))
+ mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex("http.*")))
.Returns(MediaProtocol.Http);
BaseItem.MediaSourceManager = mediaSourceManager.Object;
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs
index d136c1bc6..5aa7c04f6 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs
@@ -1,6 +1,7 @@
using System.Linq;
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.Audio;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
@@ -62,7 +63,7 @@ public class AudioResolverTests
null,
Mock.Of<ILibraryManager>())
{
- CollectionType = "books",
+ CollectionType = CollectionType.books,
FileInfo = new FileSystemMetadata
{
FullName = parent,
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
index 6d0ed7bbb..cc2e47c33 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -1,5 +1,6 @@
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.TV;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -28,7 +29,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
null)
{
Parent = parent,
- CollectionType = CollectionType.TvShows,
+ CollectionType = CollectionType.tvshows,
FileInfo = new FileSystemMetadata
{
FullName = "All My Children/Season 01/Extras/All My Children S01E01 - Behind The Scenes.mkv"
@@ -51,7 +52,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
null)
{
Parent = series,
- CollectionType = CollectionType.TvShows,
+ CollectionType = CollectionType.tvshows,
FileInfo = new FileSystemMetadata
{
FullName = "Extras/Extras S01E01.mkv"
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index c33a957e6..1c35eb3f5 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -48,10 +48,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff/", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/jeff's band", "/home/not jeff", "/home/not jeff/consistently inconsistent.mp3")]
- [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
- [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")]
- [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")]
- [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")]
+ [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
+ [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")]
+ [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")]
+ [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")]
[InlineData("/o", "/o", "/s", "/s")] // regression test for #5977
public void TryReplaceSubPath_ValidArgs_Correct(string path, string subPath, string newSubPath, string? expectedResult)
{
@@ -78,10 +78,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[Theory]
[InlineData(null, '/', null)]
[InlineData(null, '\\', null)]
- [InlineData("/home/jeff/myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")]
- [InlineData("C:\\Users\\Jeff\\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")]
- [InlineData("\\home/jeff\\myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")]
- [InlineData("\\home/jeff\\myfile.mkv", '/', "/home/jeff/myfile.mkv")]
+ [InlineData("/home/jeff/myfile.mkv", '\\', @"\home\jeff\myfile.mkv")]
+ [InlineData(@"C:\Users\Jeff\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")]
+ [InlineData(@"\home/jeff\myfile.mkv", '\\', @"\home\jeff\myfile.mkv")]
+ [InlineData(@"\home/jeff\myfile.mkv", '/', "/home/jeff/myfile.mkv")]
[InlineData("", '/', "")]
public void NormalizePath_SpecifyingSeparator_Normalizes(string path, char separator, string expectedPath)
{
@@ -90,8 +90,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[Theory]
[InlineData("/home/jeff/myfile.mkv")]
- [InlineData("C:\\Users\\Jeff\\myfile.mkv")]
- [InlineData("\\home/jeff\\myfile.mkv")]
+ [InlineData(@"C:\Users\Jeff\myfile.mkv")]
+ [InlineData(@"\home/jeff\myfile.mkv")]
public void NormalizePath_NoArgs_UsesDirectorySeparatorChar(string path)
{
var separator = Path.DirectorySeparatorChar;
@@ -101,8 +101,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[Theory]
[InlineData("/home/jeff/myfile.mkv", '/')]
- [InlineData("C:\\Users\\Jeff\\myfile.mkv", '\\')]
- [InlineData("\\home/jeff\\myfile.mkv", '/')]
+ [InlineData(@"C:\Users\Jeff\myfile.mkv", '\\')]
+ [InlineData(@"\home/jeff\myfile.mkv", '/')]
public void NormalizePath_OutVar_Correct(string path, char expectedSeparator)
{
var result = path.NormalizePath(out var separator);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
index d4b90dac0..934024826 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
@@ -119,8 +119,8 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
[InlineData("C:\\some.dll")] // Windows root path.
[InlineData("test.txt")] // Not a DLL
[InlineData(".././.././../some.dll")] // Traversal with current and parent
- [InlineData("..\\.\\..\\.\\..\\some.dll")] // Windows traversal with current and parent
- [InlineData("\\\\network\\resource.dll")] // UNC Path
+ [InlineData(@"..\.\..\.\..\some.dll")] // Windows traversal with current and parent
+ [InlineData(@"\\network\resource.dll")] // UNC Path
[InlineData("https://jellyfin.org/some.dll")] // URL
[InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character.
public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath)
@@ -191,13 +191,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
- File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
+ await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
- var resultBytes = File.ReadAllBytes(metafilePath);
+ var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
@@ -231,7 +231,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var metafilePath = Path.Combine(_pluginPath, "meta.json");
- var resultBytes = File.ReadAllBytes(metafilePath);
+ var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
@@ -251,13 +251,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
- File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
+ await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
- var resultBytes = File.ReadAllBytes(metafilePath);
+ var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
@@ -277,13 +277,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
- File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
+ await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
- var resultBytes = File.ReadAllBytes(metafilePath);
+ var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs
new file mode 100644
index 000000000..ebd3a3891
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.SessionManager;
+
+public class SessionManagerTests
+{
+ [Theory]
+ [InlineData("", typeof(ArgumentException))]
+ [InlineData(null, typeof(ArgumentNullException))]
+ public async Task GetAuthorizationToken_Should_ThrowException(string deviceId, Type exceptionType)
+ {
+ await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager(
+ NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
+ Mock.Of<IEventManager>(),
+ Mock.Of<IUserDataManager>(),
+ Mock.Of<IServerConfigurationManager>(),
+ Mock.Of<ILibraryManager>(),
+ Mock.Of<IUserManager>(),
+ Mock.Of<IMusicManager>(),
+ Mock.Of<IDtoService>(),
+ Mock.Of<IImageProcessor>(),
+ Mock.Of<IServerApplicationHost>(),
+ Mock.Of<IDeviceManager>(),
+ Mock.Of<IMediaSourceManager>(),
+ Mock.Of<IHostApplicationLifetime>());
+
+ await Assert.ThrowsAsync(exceptionType, () => sessionManager.GetAuthorizationToken(
+ new User("test", "default", "default"),
+ deviceId,
+ "app_name",
+ "0.0.0",
+ "device_name"));
+ }
+
+ [Theory]
+ [MemberData(nameof(AuthenticateNewSessionInternal_Exception_TestData))]
+ public async Task AuthenticateNewSessionInternal_Should_ThrowException(AuthenticationRequest authenticationRequest, Type exceptionType)
+ {
+ await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager(
+ NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
+ Mock.Of<IEventManager>(),
+ Mock.Of<IUserDataManager>(),
+ Mock.Of<IServerConfigurationManager>(),
+ Mock.Of<ILibraryManager>(),
+ Mock.Of<IUserManager>(),
+ Mock.Of<IMusicManager>(),
+ Mock.Of<IDtoService>(),
+ Mock.Of<IImageProcessor>(),
+ Mock.Of<IServerApplicationHost>(),
+ Mock.Of<IDeviceManager>(),
+ Mock.Of<IMediaSourceManager>(),
+ Mock.Of<IHostApplicationLifetime>());
+
+ await Assert.ThrowsAsync(exceptionType, () => sessionManager.AuthenticateNewSessionInternal(authenticationRequest, false));
+ }
+
+ public static TheoryData<AuthenticationRequest, Type> AuthenticateNewSessionInternal_Exception_TestData()
+ {
+ var data = new TheoryData<AuthenticationRequest, Type>
+ {
+ {
+ new AuthenticationRequest { App = string.Empty, DeviceId = "device_id", DeviceName = "device_name", AppVersion = "app_version" },
+ typeof(ArgumentException)
+ },
+ {
+ new AuthenticationRequest { App = null, DeviceId = "device_id", DeviceName = "device_name", AppVersion = "app_version" },
+ typeof(ArgumentNullException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = string.Empty, DeviceName = "device_name", AppVersion = "app_version" },
+ typeof(ArgumentException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = null, DeviceName = "device_name", AppVersion = "app_version" },
+ typeof(ArgumentNullException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = string.Empty, AppVersion = "app_version" },
+ typeof(ArgumentException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = null, AppVersion = "app_version" },
+ typeof(ArgumentNullException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = "device_name", AppVersion = string.Empty },
+ typeof(ArgumentException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = "device_name", AppVersion = null },
+ typeof(ArgumentNullException)
+ }
+ };
+
+ return data;
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
index 1dd49b2cf..ad85bdb6e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
@@ -9,22 +9,21 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting
{
public class AiredEpisodeOrderComparerTests
{
+ private readonly AiredEpisodeOrderComparer _cmp = new AiredEpisodeOrderComparer();
+
[Theory]
[ClassData(typeof(EpisodeBadData))]
public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y)
{
- var cmp = new AiredEpisodeOrderComparer();
- Assert.Throws<ArgumentNullException>(() => cmp.Compare(x, y));
+ Assert.Throws<ArgumentNullException>(() => _cmp.Compare(x, y));
}
[Theory]
[ClassData(typeof(EpisodeTestData))]
public void AiredEpisodeOrderCompareTest(BaseItem x, BaseItem y, int expected)
{
- var cmp = new AiredEpisodeOrderComparer();
-
- Assert.Equal(expected, cmp.Compare(x, y));
- Assert.Equal(-expected, cmp.Compare(y, x));
+ Assert.Equal(expected, _cmp.Compare(x, y));
+ Assert.Equal(-expected, _cmp.Compare(y, x));
}
private sealed class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs
index 18588bd67..52f71ef4a 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/IndexNumberComparerTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting;
public class IndexNumberComparerTests
{
- private readonly IBaseItemComparer _cmp = new IndexNumberComparer();
+ private readonly IndexNumberComparer _cmp = new IndexNumberComparer();
public static TheoryData<BaseItem?, BaseItem?> Compare_GivenNull_ThrowsArgumentNullException_TestData()
=> new()
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs
index 261092e01..bedd187eb 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/ParentIndexNumberComparerTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting;
public class ParentIndexNumberComparerTests
{
- private readonly IBaseItemComparer _cmp = new ParentIndexNumberComparer();
+ private readonly ParentIndexNumberComparer _cmp = new ParentIndexNumberComparer();
public static TheoryData<BaseItem?, BaseItem?> Compare_GivenNull_ThrowsArgumentNullException_TestData()
=> new()
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
deleted file mode 100644
index e5d5e785c..000000000
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using System;
-using System.Linq;
-using System.Net;
-using System.Net.Http.Json;
-using System.Net.Mime;
-using System.Text;
-using System.Text.Json;
-using System.Threading.Tasks;
-using Jellyfin.Extensions.Json;
-using MediaBrowser.Model.Dlna;
-using Xunit;
-using Xunit.Priority;
-
-namespace Jellyfin.Server.Integration.Tests.Controllers
-{
- [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
- public sealed class DlnaControllerTests : IClassFixture<JellyfinApplicationFactory>
- {
- private const string NonExistentProfile = "1322f35b8f2c434dad3cc07c9b97dbd1";
- private readonly JellyfinApplicationFactory _factory;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private static string? _accessToken;
- private static string? _newDeviceProfileId;
-
- public DlnaControllerTests(JellyfinApplicationFactory factory)
- {
- _factory = factory;
- }
-
- [Fact]
- [Priority(0)]
- public async Task GetProfile_DoesNotExist_NotFound()
- {
- var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
-
- using var response = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile);
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
- }
-
- [Fact]
- [Priority(0)]
- public async Task DeleteProfile_DoesNotExist_NotFound()
- {
- var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
-
- using var response = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile);
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
- }
-
- [Fact]
- [Priority(0)]
- public async Task UpdateProfile_DoesNotExist_NotFound()
- {
- var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
-
- var deviceProfile = new DeviceProfile()
- {
- Name = "ThisProfileDoesNotExist"
- };
-
- using var response = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions);
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
- }
-
- [Fact]
- [Priority(1)]
- public async Task CreateProfile_Valid_NoContent()
- {
- var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
-
- var deviceProfile = new DeviceProfile()
- {
- Name = "ThisProfileIsNew"
- };
-
- using var response = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions);
- Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
- }
-
- [Fact]
- [Priority(2)]
- public async Task GetProfileInfos_Valid_ContainsThisProfileIsNew()
- {
- var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
-
- using var response = await client.GetAsync("/Dlna/ProfileInfos");
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
- Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
-
- var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions);
-
- var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal));
- Assert.NotNull(newProfile);
- _newDeviceProfileId = newProfile!.Id;
- }
-
- [Fact]
- [Priority(3)]
- public async Task UpdateProfile_Valid_NoContent()
- {
- var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
-
- var updatedProfile = new DeviceProfile()
- {
- Name = "ThisProfileIsUpdated",
- Id = _newDeviceProfileId
- };
-
- using var postResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + _newDeviceProfileId, updatedProfile, _jsonOptions);
- Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
-
- // Verify that the profile got updated
- using var response = await client.GetAsync("/Dlna/ProfileInfos");
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
- Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
-
- var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions);
-
- Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal)));
- var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal));
- Assert.NotNull(newProfile);
- _newDeviceProfileId = newProfile!.Id;
- }
-
- [Fact]
- [Priority(5)]
- public async Task DeleteProfile_Valid_NoContent()
- {
- var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
-
- using var deleteResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId);
- Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
-
- // Verify that the profile got deleted
- using var response = await client.GetAsync("/Dlna/ProfileInfos");
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
- Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
-
- var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions);
-
- Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal)));
- }
- }
-}
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index 1c87d11f1..a078eff77 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -8,9 +8,9 @@ using Jellyfin.Server.Helpers;
using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
@@ -39,9 +39,9 @@ namespace Jellyfin.Server.Integration.Tests
}
/// <inheritdoc/>
- protected override IWebHostBuilder CreateWebHostBuilder()
+ protected override IHostBuilder CreateHostBuilder()
{
- return new WebHostBuilder();
+ return new HostBuilder();
}
/// <inheritdoc/>
@@ -95,18 +95,17 @@ namespace Jellyfin.Server.Integration.Tests
}
/// <inheritdoc/>
- protected override TestServer CreateServer(IWebHostBuilder builder)
+ protected override IHost CreateHost(IHostBuilder builder)
{
- // Create the test server using the base implementation
- var testServer = base.CreateServer(builder);
-
- // Finish initializing the app host
- var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
- appHost.ServiceProvider = testServer.Services;
+ var host = builder.Build();
+ var appHost = (TestAppHost)host.Services.GetRequiredService<IApplicationHost>();
+ appHost.ServiceProvider = host.Services;
appHost.InitializeServices().GetAwaiter().GetResult();
+ host.Start();
+
appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
- return testServer;
+ return host;
}
/// <inheritdoc/>
diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
index 288102037..123266d29 100644
--- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
+++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
@@ -1,16 +1,17 @@
using System;
using System.Linq;
using System.Net;
-using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using Jellyfin.Server.Extensions;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
+using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
namespace Jellyfin.Server.Tests
{
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index f56f58c6f..0a153b9cc 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -60,7 +60,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
{
Exists = true,
FullName = OperatingSystem.IsWindows() ?
- "C:\\media\\movies\\Justice League (2017).jpg"
+ @"C:\media\movies\Justice League (2017).jpg"
: "/media/movies/Justice League (2017).jpg"
};
directoryService.Setup(x => x.GetFile(_localImageFileMetadata.FullName))