aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-api-client.yml59
-rw-r--r--.ci/azure-pipelines-package.yml4
-rw-r--r--.ci/azure-pipelines-test.yml2
-rw-r--r--.ci/azure-pipelines.yml3
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md8
-rw-r--r--.github/dependabot.yml8
-rw-r--r--.github/workflows/automation.yml74
-rw-r--r--.github/workflows/commands.yml119
-rw-r--r--.gitignore1
-rw-r--r--CONTRIBUTORS.md7
-rw-r--r--Dockerfile8
-rw-r--r--Dockerfile.arm6
-rw-r--r--Dockerfile.arm646
-rw-r--r--Emby.Dlna/Configuration/DlnaOptions.cs2
-rw-r--r--Emby.Dlna/ConfigurationExtension.cs1
-rw-r--r--Emby.Dlna/ConnectionManager/ControlHandler.cs2
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectoryService.cs2
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs49
-rw-r--r--Emby.Dlna/ContentDirectory/StubType.cs1
-rw-r--r--Emby.Dlna/ControlRequest.cs2
-rw-r--r--Emby.Dlna/ControlResponse.cs2
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs37
-rw-r--r--Emby.Dlna/Didl/StringWriterWithEncoding.cs2
-rw-r--r--Emby.Dlna/DlnaConfigurationFactory.cs1
-rw-r--r--Emby.Dlna/DlnaManager.cs127
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj2
-rw-r--r--Emby.Dlna/EventSubscriptionResponse.cs2
-rw-r--r--Emby.Dlna/Eventing/DlnaEventManager.cs2
-rw-r--r--Emby.Dlna/Eventing/EventSubscription.cs2
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs20
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs2
-rw-r--r--Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs1
-rw-r--r--Emby.Dlna/PlayTo/Device.cs134
-rw-r--r--Emby.Dlna/PlayTo/DeviceInfo.cs2
-rw-r--r--Emby.Dlna/PlayTo/MediaChangedEventArgs.cs2
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs61
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs9
-rw-r--r--Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs2
-rw-r--r--Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs2
-rw-r--r--Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs2
-rw-r--r--Emby.Dlna/PlayTo/PlaylistItem.cs2
-rw-r--r--Emby.Dlna/PlayTo/PlaylistItemFactory.cs2
-rw-r--r--Emby.Dlna/PlayTo/SsdpHttpClient.cs26
-rw-r--r--Emby.Dlna/PlayTo/TransportCommands.cs20
-rw-r--r--Emby.Dlna/PlayTo/TransportState.cs1
-rw-r--r--Emby.Dlna/PlayTo/uBaseObject.cs2
-rw-r--r--Emby.Dlna/Profiles/DefaultProfile.cs3
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs3
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs6
-rw-r--r--Emby.Dlna/Ssdp/DeviceDiscovery.cs6
-rw-r--r--Emby.Dlna/Ssdp/SsdpExtensions.cs6
-rw-r--r--Emby.Drawing/Emby.Drawing.csproj1
-rw-r--r--Emby.Drawing/ImageProcessor.cs125
-rw-r--r--Emby.Drawing/NullImageEncoder.cs2
-rw-r--r--Emby.Naming/Audio/AudioFileParser.cs6
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs2
-rw-r--r--Emby.Naming/Emby.Naming.csproj6
-rw-r--r--Emby.Naming/TV/EpisodeResolver.cs10
-rw-r--r--Emby.Naming/Video/CleanStringParser.cs11
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs95
-rw-r--r--Emby.Naming/Video/FlagParser.cs53
-rw-r--r--Emby.Naming/Video/Format3DParser.cs88
-rw-r--r--Emby.Naming/Video/Format3DResult.cs23
-rw-r--r--Emby.Naming/Video/StackResolver.cs4
-rw-r--r--Emby.Naming/Video/VideoFileInfo.cs7
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs230
-rw-r--r--Emby.Naming/Video/VideoResolver.cs77
-rw-r--r--Emby.Notifications/Emby.Notifications.csproj7
-rw-r--r--Emby.Photos/Emby.Photos.csproj7
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs8
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs46
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs9
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs142
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs5
-rw-r--r--Emby.Server.Implementations/Collections/CollectionImageProvider.cs4
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs67
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs2
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs1
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs2
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs8
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs6
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs131
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs1498
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs22
-rw-r--r--Emby.Server.Implementations/Data/TypeMapper.cs20
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs2
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs2
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs17
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj16
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs11
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs2
-rw-r--r--Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs2
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs20
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs8
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs1
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs54
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs6
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs6
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs2
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs2
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs128
-rw-r--r--Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs2
-rw-r--r--Emby.Server.Implementations/IO/StreamHelper.cs2
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs12
-rw-r--r--Emby.Server.Implementations/Images/ArtistImageProvider.cs8
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs4
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/DynamicImageProvider.cs3
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/PlaylistImageProvider.cs6
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs2
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs2
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs273
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs4
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs38
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs2
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs5
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs76
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs62
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs39
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs14
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs224
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs14
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs46
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs43
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs3
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs16
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs37
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs50
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs28
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs25
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs (renamed from Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs)46
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs39
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs329
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs52
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs8
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json23
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json83
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json60
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json50
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json56
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nn.json73
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs52
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs10
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs2
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs2
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs4
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs110
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs18
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs43
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs10
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs101
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs1
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs39
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs38
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs26
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs52
-rw-r--r--Emby.Server.Implementations/Security/AuthenticationRepository.cs45
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs11
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs10
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs40
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs2
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs4
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumComparer.cs4
-rw-r--r--Emby.Server.Implementations/Sorting/ArtistComparer.cs4
-rw-r--r--Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/CriticRatingComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/DateCreatedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/IsFolderComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/IsPlayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/NameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/PremiereDateComparer.cs9
-rw-r--r--Emby.Server.Implementations/Sorting/ProductionYearComparer.cs9
-rw-r--r--Emby.Server.Implementations/Sorting/RandomComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs2
-rw-r--r--Emby.Server.Implementations/SyncPlay/Group.cs2
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs25
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs62
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs72
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs60
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs5
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs14
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs32
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs8
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs47
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs201
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs7
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs27
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs23
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs196
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs90
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs90
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs20
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs16
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs15
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs20
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs7
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs21
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs20
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs10
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs55
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs5
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs32
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs5
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs66
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs20
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs28
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs45
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs62
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs10
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs30
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs42
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs12
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs5
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs13
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileCopier.cs3
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs2
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs39
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs17
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj7
-rw-r--r--Jellyfin.Api/Models/ConfigurationPageInfo.cs9
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs15
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs19
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs23
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs1
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs30
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs28
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs5
-rw-r--r--Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs5
-rw-r--r--Jellyfin.Data/DayOfWeekHelper.cs60
-rw-r--r--Jellyfin.Data/Entities/AccessSchedule.cs37
-rw-r--r--Jellyfin.Data/Entities/ActivityLog.cs28
-rw-r--r--Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs26
-rw-r--r--Jellyfin.Data/Entities/DisplayPreferences.cs26
-rw-r--r--Jellyfin.Data/Entities/Group.cs27
-rw-r--r--Jellyfin.Data/Entities/HomeSection.cs8
-rw-r--r--Jellyfin.Data/Entities/ImageInfo.cs19
-rw-r--r--Jellyfin.Data/Entities/ItemDisplayPreferences.cs13
-rw-r--r--Jellyfin.Data/Entities/Libraries/Artwork.cs24
-rw-r--r--Jellyfin.Data/Entities/Libraries/Book.cs11
-rw-r--r--Jellyfin.Data/Entities/Libraries/BookMetadata.cs29
-rw-r--r--Jellyfin.Data/Entities/Libraries/Chapter.cs31
-rw-r--r--Jellyfin.Data/Entities/Libraries/Collection.cs14
-rw-r--r--Jellyfin.Data/Entities/Libraries/CollectionItem.cs42
-rw-r--r--Jellyfin.Data/Entities/Libraries/Company.cs33
-rw-r--r--Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs25
-rw-r--r--Jellyfin.Data/Entities/Libraries/CustomItem.cs11
-rw-r--r--Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs21
-rw-r--r--Jellyfin.Data/Entities/Libraries/Episode.cs30
-rw-r--r--Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs26
-rw-r--r--Jellyfin.Data/Entities/Libraries/Genre.cs33
-rw-r--r--Jellyfin.Data/Entities/Libraries/ItemMetadata.cs53
-rw-r--r--Jellyfin.Data/Entities/Libraries/Library.cs28
-rw-r--r--Jellyfin.Data/Entities/Libraries/LibraryItem.cs18
-rw-r--r--Jellyfin.Data/Entities/Libraries/MediaFile.cs33
-rw-r--r--Jellyfin.Data/Entities/Libraries/MediaFileStream.cs29
-rw-r--r--Jellyfin.Data/Entities/Libraries/MetadataProvider.cs17
-rw-r--r--Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs29
-rw-r--r--Jellyfin.Data/Entities/Libraries/Movie.cs11
-rw-r--r--Jellyfin.Data/Entities/Libraries/MovieMetadata.cs31
-rw-r--r--Jellyfin.Data/Entities/Libraries/MusicAlbum.cs13
-rw-r--r--Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs27
-rw-r--r--Jellyfin.Data/Entities/Libraries/Person.cs29
-rw-r--r--Jellyfin.Data/Entities/Libraries/PersonRole.cs42
-rw-r--r--Jellyfin.Data/Entities/Libraries/Photo.cs11
-rw-r--r--Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs21
-rw-r--r--Jellyfin.Data/Entities/Libraries/Rating.cs29
-rw-r--r--Jellyfin.Data/Entities/Libraries/RatingSource.cs31
-rw-r--r--Jellyfin.Data/Entities/Libraries/Release.cs32
-rw-r--r--Jellyfin.Data/Entities/Libraries/Season.cs32
-rw-r--r--Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs22
-rw-r--r--Jellyfin.Data/Entities/Libraries/Series.cs14
-rw-r--r--Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs37
-rw-r--r--Jellyfin.Data/Entities/Libraries/Track.cs30
-rw-r--r--Jellyfin.Data/Entities/Libraries/TrackMetadata.cs21
-rw-r--r--Jellyfin.Data/Entities/Permission.cs26
-rw-r--r--Jellyfin.Data/Entities/Preference.cs24
-rw-r--r--Jellyfin.Data/Entities/User.cs100
-rw-r--r--Jellyfin.Data/Enums/HomeSectionType.cs7
-rw-r--r--Jellyfin.Data/Interfaces/IHasPermissions.cs2
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj11
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj16
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs98
-rw-r--r--Jellyfin.Drawing.Skia/StripCollageBuilder.cs14
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfiguration.cs1
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs1
-rw-r--r--Jellyfin.Networking/Jellyfin.Networking.csproj7
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs190
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs8
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs14
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs6
-rw-r--r--Jellyfin.Server.Implementations/Events/EventManager.cs9
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj9
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs68
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs535
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.cs240
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs520
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.cs35
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs43
-rw-r--r--Jellyfin.Server.Implementations/Properties/AssemblyInfo.cs23
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs5
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs74
-rw-r--r--Jellyfin.Server/CoreAppHost.cs5
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs10
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs11
-rw-r--r--Jellyfin.Server/Filters/AdditionalModelFilter.cs (renamed from Jellyfin.Server/Filters/WebsocketModelFilter.cs)8
-rw-r--r--Jellyfin.Server/Filters/ParameterObsoleteFilter.cs1
-rw-r--r--Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs2
-rw-r--r--Jellyfin.Server/Formatters/CssOutputFormatter.cs3
-rw-r--r--Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs2
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj16
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs32
-rw-r--r--Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs31
-rw-r--r--Jellyfin.Server/Middleware/LanFilteringMiddleware.cs3
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs35
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs91
-rw-r--r--Jellyfin.Server/Migrations/MigrationOptions.cs3
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs8
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs6
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs10
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs17
-rw-r--r--Jellyfin.Server/Program.cs25
-rw-r--r--Jellyfin.Server/Properties/AssemblyInfo.cs2
-rw-r--r--Jellyfin.Server/Startup.cs15
-rw-r--r--Jellyfin.Server/StartupOptions.cs7
-rw-r--r--Jellyfin.sln51
-rw-r--r--MediaBrowser.Common/Configuration/ConfigurationStore.cs2
-rw-r--r--MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs1
-rw-r--r--MediaBrowser.Common/Configuration/IApplicationPaths.cs2
-rw-r--r--MediaBrowser.Common/Crc32.cs89
-rw-r--r--MediaBrowser.Common/Cryptography/PasswordHash.cs121
-rw-r--r--MediaBrowser.Common/Events/EventHelper.cs4
-rw-r--r--MediaBrowser.Common/Extensions/BaseExtensions.cs2
-rw-r--r--MediaBrowser.Common/Extensions/CopyToExtensions.cs2
-rw-r--r--MediaBrowser.Common/Extensions/EnumerableExtensions.cs51
-rw-r--r--MediaBrowser.Common/Extensions/HttpContextExtensions.cs6
-rw-r--r--MediaBrowser.Common/Extensions/MethodNotAllowedException.cs2
-rw-r--r--MediaBrowser.Common/Extensions/ProcessExtensions.cs2
-rw-r--r--MediaBrowser.Common/Extensions/RateLimitExceededException.cs1
-rw-r--r--MediaBrowser.Common/Extensions/ResourceNotFoundException.cs2
-rw-r--r--MediaBrowser.Common/Extensions/ShuffleExtensions.cs2
-rw-r--r--MediaBrowser.Common/Extensions/SplitStringExtensions.cs95
-rw-r--r--MediaBrowser.Common/Extensions/StreamExtensions.cs63
-rw-r--r--MediaBrowser.Common/Extensions/StringBuilderExtensions.cs35
-rw-r--r--MediaBrowser.Common/Extensions/StringExtensions.cs37
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs8
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs57
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs4
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs81
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs6
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs2
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs18
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs57
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs4
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonStringConverter.cs39
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs5
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs9
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj10
-rw-r--r--MediaBrowser.Common/Net/DefaultHttpClientHandler.cs19
-rw-r--r--MediaBrowser.Common/Net/INetworkManager.cs9
-rw-r--r--MediaBrowser.Common/Net/IPHost.cs111
-rw-r--r--MediaBrowser.Common/Net/IPNetAddress.cs23
-rw-r--r--MediaBrowser.Common/Net/IPObject.cs17
-rw-r--r--MediaBrowser.Common/Net/NetworkExtensions.cs18
-rw-r--r--MediaBrowser.Common/Plugins/BasePlugin.cs7
-rw-r--r--MediaBrowser.Common/Plugins/BasePluginOfT.cs43
-rw-r--r--MediaBrowser.Common/Plugins/IPlugin.cs2
-rw-r--r--MediaBrowser.Common/Plugins/IPluginManager.cs16
-rw-r--r--MediaBrowser.Common/Plugins/LocalPlugin.cs7
-rw-r--r--MediaBrowser.Common/Plugins/PluginManifest.cs2
-rw-r--r--MediaBrowser.Common/Progress/ActionableProgress.cs5
-rw-r--r--MediaBrowser.Common/Progress/SimpleProgress.cs3
-rw-r--r--MediaBrowser.Common/Providers/ProviderIdParsers.cs123
-rw-r--r--MediaBrowser.Common/Updates/IInstallationManager.cs8
-rw-r--r--MediaBrowser.Common/Updates/InstallationEventArgs.cs2
-rw-r--r--MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs1
-rw-r--r--MediaBrowser.Controller/Authentication/AuthenticationResult.cs2
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs2
-rw-r--r--MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs2
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs12
-rw-r--r--MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs3
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs4
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemInfo.cs28
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemResult.cs10
-rw-r--r--MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs11
-rw-r--r--MediaBrowser.Controller/Channels/ChannelSearchInfo.cs7
-rw-r--r--MediaBrowser.Controller/Channels/IChannel.cs2
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs30
-rw-r--r--MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs12
-rw-r--r--MediaBrowser.Controller/Channels/IHasCacheKey.cs2
-rw-r--r--MediaBrowser.Controller/Channels/IHasFolderAttributes.cs9
-rw-r--r--MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs8
-rw-r--r--MediaBrowser.Controller/Channels/ISearchableChannel.cs34
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsDelete.cs15
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs21
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs9
-rw-r--r--MediaBrowser.Controller/Channels/InternalChannelFeatures.cs6
-rw-r--r--MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs2
-rw-r--r--MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs24
-rw-r--r--MediaBrowser.Controller/Collections/CollectionCreationOptions.cs6
-rw-r--r--MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs (renamed from MediaBrowser.Controller/Collections/CollectionEvents.cs)25
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs2
-rw-r--r--MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs2
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs6
-rw-r--r--MediaBrowser.Controller/Dlna/IDlnaManager.cs2
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs1
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs3
-rw-r--r--MediaBrowser.Controller/Drawing/ImageCollageOptions.cs6
-rw-r--r--MediaBrowser.Controller/Drawing/ImageHelper.cs68
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs14
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs2
-rw-r--r--MediaBrowser.Controller/Drawing/ImageStream.cs12
-rw-r--r--MediaBrowser.Controller/Dto/DtoOptions.cs47
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs8
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs11
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs14
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs8
-rw-r--r--MediaBrowser.Controller/Entities/AudioBook.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs153
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs19
-rw-r--r--MediaBrowser.Controller/Entities/BasePluginFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Book.cs12
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs82
-rw-r--r--MediaBrowser.Controller/Entities/Extensions.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs250
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs44
-rw-r--r--MediaBrowser.Controller/Entities/ICollectionFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasAspectRatio.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasDisplayOrder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasMediaSources.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasProgramAttributes.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasScreenshots.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasSeries.cs4
-rw-r--r--MediaBrowser.Controller/Entities/IHasShares.cs11
-rw-r--r--MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasTrailers.cs2
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs88
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs24
-rw-r--r--MediaBrowser.Controller/Entities/ItemImageInfo.cs2
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChild.cs47
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChildComparer.cs35
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChildType.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs5
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs6
-rw-r--r--MediaBrowser.Controller/Entities/MusicVideo.cs14
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs11
-rw-r--r--MediaBrowser.Controller/Entities/PersonInfo.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Photo.cs5
-rw-r--r--MediaBrowser.Controller/Entities/Share.cs7
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs8
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs32
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs23
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs34
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs6
-rw-r--r--MediaBrowser.Controller/Entities/UserItemData.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs11
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs29
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs30
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs5
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs12
-rw-r--r--MediaBrowser.Controller/Events/IEventConsumer.cs2
-rw-r--r--MediaBrowser.Controller/Events/IEventManager.cs2
-rw-r--r--MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs1
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Extensions/StringExtensions.cs24
-rw-r--r--MediaBrowser.Controller/IDisplayPreferencesManager.cs4
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs2
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs10
-rw-r--r--MediaBrowser.Controller/IServerApplicationPaths.cs2
-rw-r--r--MediaBrowser.Controller/Library/DeleteOptions.cs8
-rw-r--r--MediaBrowser.Controller/Library/IIntroProvider.cs14
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs32
-rw-r--r--MediaBrowser.Controller/Library/ILiveStream.cs2
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IMetadataSaver.cs2
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs13
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs14
-rw-r--r--MediaBrowser.Controller/Library/IUserViewManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IntroInfo.cs2
-rw-r--r--MediaBrowser.Controller/Library/ItemChangeEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs35
-rw-r--r--MediaBrowser.Controller/Library/LibraryManagerExtensions.cs2
-rw-r--r--MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs17
-rw-r--r--MediaBrowser.Controller/Library/MetadataConfigurationStore.cs12
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs9
-rw-r--r--MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/Profiler.cs83
-rw-r--r--MediaBrowser.Controller/Library/SearchHintInfo.cs2
-rw-r--r--MediaBrowser.Controller/Library/TVUtils.cs4
-rw-r--r--MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs19
-rw-r--r--MediaBrowser.Controller/LiveTv/ChannelInfo.cs14
-rw-r--r--MediaBrowser.Controller/LiveTv/IListingsProvider.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs18
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvService.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHost.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs23
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ProgramInfo.cs35
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingInfo.cs28
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs31
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerEventInfo.cs1
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerInfo.cs17
-rw-r--r--MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs2
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj15
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs (renamed from MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs)54
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs77
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs55
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs23
-rw-r--r--MediaBrowser.Controller/Net/AuthorizationInfo.cs2
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs11
-rw-r--r--MediaBrowser.Controller/Net/IAuthService.cs2
-rw-r--r--MediaBrowser.Controller/Net/ISessionContext.cs4
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs8
-rw-r--r--MediaBrowser.Controller/Net/SecurityException.cs2
-rw-r--r--MediaBrowser.Controller/Net/WebSocketListenerState.cs17
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessageInfo.cs2
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationManager.cs2
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationService.cs2
-rw-r--r--MediaBrowser.Controller/Notifications/UserNotification.cs2
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs17
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs13
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs34
-rw-r--r--MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs22
-rw-r--r--MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs9
-rw-r--r--MediaBrowser.Controller/Plugins/IServerEntryPoint.cs8
-rw-r--r--MediaBrowser.Controller/Providers/AlbumInfo.cs14
-rw-r--r--MediaBrowser.Controller/Providers/ArtistInfo.cs4
-rw-r--r--MediaBrowser.Controller/Providers/BookInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs40
-rw-r--r--MediaBrowser.Controller/Providers/DynamicImageResponse.cs2
-rw-r--r--MediaBrowser.Controller/Providers/EpisodeInfo.cs12
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs4
-rw-r--r--MediaBrowser.Controller/Providers/IExternalId.cs2
-rw-r--r--MediaBrowser.Controller/Providers/ILocalImageProvider.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IMetadataService.cs12
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs16
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteImageProvider.cs1
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs1
-rw-r--r--MediaBrowser.Controller/Providers/ImageRefreshOptions.cs2
-rw-r--r--MediaBrowser.Controller/Providers/ItemInfo.cs5
-rw-r--r--MediaBrowser.Controller/Providers/ItemLookupInfo.cs20
-rw-r--r--MediaBrowser.Controller/Providers/LocalImageInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs38
-rw-r--r--MediaBrowser.Controller/Providers/MetadataResult.cs40
-rw-r--r--MediaBrowser.Controller/Providers/MusicVideoInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/RefreshPriority.cs23
-rw-r--r--MediaBrowser.Controller/Providers/RemoteSearchQuery.cs4
-rw-r--r--MediaBrowser.Controller/Providers/SeasonInfo.cs4
-rw-r--r--MediaBrowser.Controller/Providers/SongInfo.cs2
-rw-r--r--MediaBrowser.Controller/QuickConnect/IQuickConnect.cs2
-rw-r--r--MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs2
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs (renamed from MediaBrowser.Controller/Resolvers/BaseItemResolver.cs)18
-rw-r--r--MediaBrowser.Controller/Security/AuthenticationInfo.cs2
-rw-r--r--MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs2
-rw-r--r--MediaBrowser.Controller/Security/IAuthenticationRepository.cs2
-rw-r--r--MediaBrowser.Controller/Session/AuthenticationRequest.cs3
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs2
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs6
-rw-r--r--MediaBrowser.Controller/Session/SessionEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs6
-rw-r--r--MediaBrowser.Controller/Sorting/AlphanumComparator.cs4
-rw-r--r--MediaBrowser.Controller/Sorting/IBaseItemComparer.cs2
-rw-r--r--MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs2
-rw-r--r--MediaBrowser.Controller/Sorting/SortExtensions.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs3
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleResponse.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs2
-rw-r--r--MediaBrowser.Controller/Sync/IHasDynamicAccess.cs20
-rw-r--r--MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs9
-rw-r--r--MediaBrowser.Controller/Sync/IServerSyncProvider.cs30
-rw-r--r--MediaBrowser.Controller/Sync/ISyncProvider.cs29
-rw-r--r--MediaBrowser.Controller/Sync/SyncedFileInfo.cs41
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupMember.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupState.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs2
-rw-r--r--MediaBrowser.Controller/TV/ITVSeriesManager.cs10
-rw-r--r--MediaBrowser.LocalMetadata/BaseXmlProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs32
-rw-r--r--MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs7
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs31
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj7
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs7
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs6
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs6
-rw-r--r--MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs28
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs1
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs8
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs16
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs15
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs109
-rw-r--r--MediaBrowser.MediaEncoding/FfmpegException.cs39
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj9
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs15
-rw-r--r--MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaChapter.cs1
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs181
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/AssParser.cs133
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs10
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs103
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs476
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs61
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs125
-rw-r--r--MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs41
-rw-r--r--MediaBrowser.Model/Channels/ChannelFeatures.cs18
-rw-r--r--MediaBrowser.Model/Channels/ChannelQuery.cs6
-rw-r--r--MediaBrowser.Model/Configuration/AccessSchedule.cs27
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs74
-rw-r--r--MediaBrowser.Model/Configuration/ImageOption.cs10
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs403
-rw-r--r--MediaBrowser.Model/Configuration/MediaPathInfo.cs12
-rw-r--r--MediaBrowser.Model/Configuration/MetadataConfiguration.cs4
-rw-r--r--MediaBrowser.Model/Configuration/MetadataOptions.cs20
-rw-r--r--MediaBrowser.Model/Configuration/MetadataPluginSummary.cs12
-rw-r--r--MediaBrowser.Model/Configuration/PathSubstitution.cs2
-rw-r--r--MediaBrowser.Model/Configuration/TypeOptions.cs365
-rw-r--r--MediaBrowser.Model/Configuration/UserConfiguration.cs36
-rw-r--r--MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs16
-rw-r--r--MediaBrowser.Model/Dlna/AudioOptions.cs9
-rw-r--r--MediaBrowser.Model/Dlna/CodecProfile.cs12
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs2
-rw-r--r--MediaBrowser.Model/Dlna/ContainerProfile.cs59
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs123
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs149
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs8
-rw-r--r--MediaBrowser.Model/Dlna/IDeviceDiscovery.cs1
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs41
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionConfiguration.cs8
-rw-r--r--MediaBrowser.Model/Dlna/ResponseProfile.cs10
-rw-r--r--MediaBrowser.Model/Dlna/SearchCriteria.cs54
-rw-r--r--MediaBrowser.Model/Dlna/SortCriteria.cs4
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs55
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs962
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs11
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs22
-rw-r--r--MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs2
-rw-r--r--MediaBrowser.Model/Drawing/DrawingUtils.cs46
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs10
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs67
-rw-r--r--MediaBrowser.Model/Dto/MetadataEditorInfo.cs18
-rw-r--r--MediaBrowser.Model/Dto/NameGuidPair.cs14
-rw-r--r--MediaBrowser.Model/Dto/NameIdPair.cs9
-rw-r--r--MediaBrowser.Model/Dto/UserDto.cs18
-rw-r--r--MediaBrowser.Model/Entities/CollectionType.cs32
-rw-r--r--MediaBrowser.Model/Entities/CollectionTypeOptions.cs16
-rw-r--r--MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs29
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs222
-rw-r--r--MediaBrowser.Model/Entities/PackageReviewInfo.cs40
-rw-r--r--MediaBrowser.Model/Entities/ProviderIdsExtensions.cs91
-rw-r--r--MediaBrowser.Model/Entities/SpecialFolder.cs36
-rw-r--r--MediaBrowser.Model/Entities/VirtualFolderInfo.cs20
-rw-r--r--MediaBrowser.Model/Extensions/StringHelper.cs3
-rw-r--r--MediaBrowser.Model/Globalization/CultureDto.cs12
-rw-r--r--MediaBrowser.Model/IO/FileSystemMetadata.cs6
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs21
-rw-r--r--MediaBrowser.Model/IO/IShortcutHandler.cs2
-rw-r--r--MediaBrowser.Model/IO/IStreamHelper.cs2
-rw-r--r--MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs16
-rw-r--r--MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs58
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs26
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvOptions.cs97
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs10
-rw-r--r--MediaBrowser.Model/LiveTv/RecordingQuery.cs16
-rw-r--r--MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs16
-rw-r--r--MediaBrowser.Model/LiveTv/TunerHostInfo.cs35
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj12
-rw-r--r--MediaBrowser.Model/MediaInfo/MediaInfo.cs24
-rw-r--r--MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs22
-rw-r--r--MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs16
-rw-r--r--MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs7
-rw-r--r--MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs4
-rw-r--r--MediaBrowser.Model/Net/ISocket.cs6
-rw-r--r--MediaBrowser.Model/Net/ISocketFactory.cs3
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs21
-rw-r--r--MediaBrowser.Model/Net/NetworkShare.cs33
-rw-r--r--MediaBrowser.Model/Net/SocketReceiveResult.cs4
-rw-r--r--MediaBrowser.Model/Net/WebSocketMessage.cs2
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOptions.cs17
-rw-r--r--MediaBrowser.Model/Notifications/NotificationRequest.cs14
-rw-r--r--MediaBrowser.Model/Plugins/PluginInfo.cs2
-rw-r--r--MediaBrowser.Model/Plugins/PluginPageInfo.cs2
-rw-r--r--MediaBrowser.Model/Providers/ExternalIdInfo.cs4
-rw-r--r--MediaBrowser.Model/Providers/RemoteImageInfo.cs2
-rw-r--r--MediaBrowser.Model/Providers/SubtitleOptions.cs16
-rw-r--r--MediaBrowser.Model/Querying/EpisodeQuery.cs75
-rw-r--r--MediaBrowser.Model/Querying/LatestItemsQuery.cs9
-rw-r--r--MediaBrowser.Model/Querying/MovieRecommendationQuery.cs47
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs24
-rw-r--r--MediaBrowser.Model/Querying/QueryFilters.cs25
-rw-r--r--MediaBrowser.Model/Querying/QueryFiltersLegacy.cs26
-rw-r--r--MediaBrowser.Model/Querying/QueryResult.cs26
-rw-r--r--MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs64
-rw-r--r--MediaBrowser.Model/Search/SearchQuery.cs32
-rw-r--r--MediaBrowser.Model/Session/BrowseRequest.cs1
-rw-r--r--MediaBrowser.Model/Session/ClientCapabilities.cs14
-rw-r--r--MediaBrowser.Model/Session/GeneralCommand.cs13
-rw-r--r--MediaBrowser.Model/Session/MessageCommand.cs3
-rw-r--r--MediaBrowser.Model/Session/PlaybackProgressInfo.cs14
-rw-r--r--MediaBrowser.Model/Session/PlaystateCommand.cs6
-rw-r--r--MediaBrowser.Model/Session/QueueItem.cs14
-rw-r--r--MediaBrowser.Model/Session/RepeatMode.cs11
-rw-r--r--MediaBrowser.Model/Session/TranscodeReason.cs31
-rw-r--r--MediaBrowser.Model/Session/TranscodingInfo.cs37
-rw-r--r--MediaBrowser.Model/Sync/SyncCategory.cs22
-rw-r--r--MediaBrowser.Model/Sync/SyncJob.cs135
-rw-r--r--MediaBrowser.Model/Sync/SyncJobStatus.cs15
-rw-r--r--MediaBrowser.Model/Sync/SyncTarget.cs20
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs18
-rw-r--r--MediaBrowser.Model/System/WakeOnLanInfo.cs2
-rw-r--r--MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs5
-rw-r--r--MediaBrowser.Model/Tasks/ITaskManager.cs20
-rw-r--r--MediaBrowser.Model/Tasks/ITaskTrigger.cs10
-rw-r--r--MediaBrowser.Model/Tasks/TaskInfo.cs16
-rw-r--r--MediaBrowser.Model/Tasks/TaskTriggerInfo.cs12
-rw-r--r--MediaBrowser.Model/Updates/InstallationInfo.cs7
-rw-r--r--MediaBrowser.Model/Updates/PackageInfo.cs4
-rw-r--r--MediaBrowser.Model/Updates/VersionInfo.cs2
-rw-r--r--MediaBrowser.Model/Users/UserAction.cs24
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs100
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs13
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs43
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs122
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs47
-rw-r--r--MediaBrowser.Providers/Manager/ProviderUtils.cs2
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj11
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs5
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs19
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs156
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs82
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs175
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs130
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs48
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs29
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs91
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs81
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs45
-rw-r--r--MediaBrowser.Providers/Studios/StudiosImageProvider.cs27
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs64
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj7
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs314
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs49
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs31
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs20
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs22
-rw-r--r--MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs27
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs27
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs18
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs27
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs19
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs23
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs27
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs27
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs23
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs18
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs13
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs18
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs31
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs17
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs11
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs21
-rw-r--r--README.md5
-rw-r--r--RSSDP/DeviceAvailableEventArgs.cs2
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs12
-rw-r--r--RSSDP/SsdpDeviceLocator.cs40
-rw-r--r--apiclient/.openapi-generator-ignore2
-rw-r--r--apiclient/templates/typescript/axios/generate.sh11
-rw-r--r--apiclient/templates/typescript/axios/package.mustache30
-rw-r--r--debian/conf/jellyfin5
-rw-r--r--debian/postinst12
-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.fedora.amd644
-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.macos2
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rw-r--r--fedora/jellyfin.env4
-rw-r--r--fuzz/.gitignore1
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj18
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Program.cs32
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt1
-rwxr-xr-xfuzz/Emby.Server.Implementations.Fuzz/fuzz.sh11
-rw-r--r--fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj22
-rw-r--r--fuzz/Jellyfin.Server.Fuzz/Program.cs33
-rw-r--r--fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt1
-rwxr-xr-xfuzz/Jellyfin.Server.Fuzz/fuzz.sh11
-rw-r--r--jellyfin.ruleset30
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs2
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs3
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs58
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj24
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs6
-rw-r--r--tests/Jellyfin.Api.Tests/TestHelpers.cs7
-rw-r--r--tests/Jellyfin.Common.Tests/Crc32Tests.cs33
-rw-r--r--tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs185
-rw-r--r--tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs40
-rw-r--r--tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs43
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj12
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs33
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs52
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs1
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs1
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs27
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs38
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs36
-rw-r--r--tests/Jellyfin.Common.Tests/PasswordHashTests.cs31
-rw-r--r--tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs85
-rw-r--r--tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs200
-rw-r--r--tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs19
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj12
-rw-r--r--tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs131
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj12
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj11
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs75
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/SsaParserTests.cs96
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs15
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs88
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json111
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json74
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/example.ssa20
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs19
-rw-r--r--tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs70
-rw-r--r--tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs196
-rw-r--r--tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs13
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj12
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs5
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj13
-rw-r--r--tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs33
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs8
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs9
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs231
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StubTests.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs308
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs42
-rw-r--r--tests/Jellyfin.Networking.Tests/IPHostTests.cs53
-rw-r--r--tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs49
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj (renamed from tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj)23
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs63
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs (renamed from tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs)215
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj37
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs96
-rw-r--r--tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs27
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs299
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs34
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj20
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs72
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs31
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs47
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs326
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs115
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/discover.json)0
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/lineup.json)0
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json684
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs82
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs28
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs59
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs30
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs14
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs (renamed from tests/Jellyfin.Api.Tests/BrandingServiceTests.cs)19
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs86
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs53
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs119
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs170
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs48
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj47
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs (renamed from tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs)18
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs (renamed from tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs)4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs54
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/TestPage.html9
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs43
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/TestPluginWithoutPages.cs27
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/WebSocketTests.cs32
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/xunit.runner.json4
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj36
-rw-r--r--tests/Jellyfin.Server.Tests/ParseNetworkTests.cs (renamed from tests/Jellyfin.Api.Tests/ParseNetworkTests.cs)19
-rw-r--r--tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs31
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj13
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs65
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs41
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs146
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs21
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs16
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicVideoNfoParserTests.cs80
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs19
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs29
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo6
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Dancing Queen.nfo50
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Imdb.nfo1
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo26
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Radarr.nfo2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo20
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo4
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Tmdb.nfo1
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Tvdb.nfo1
1066 files changed, 19083 insertions, 12177 deletions
diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml
deleted file mode 100644
index 0e944e6f4..000000000
--- a/.ci/azure-pipelines-api-client.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-parameters:
- - name: LinuxImage
- type: string
- default: "ubuntu-latest"
- - name: GeneratorVersion
- type: string
- default: "5.0.1"
-
-jobs:
-- job: GenerateApiClients
- displayName: 'Generate Api Clients'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- dependsOn: Test
-
- pool:
- vmImage: "${{ parameters.LinuxImage }}"
-
- steps:
- - task: DownloadPipelineArtifact@2
- displayName: 'Download OpenAPI Spec Artifact'
- inputs:
- source: 'current'
- artifact: "OpenAPI Spec"
- path: "$(System.ArtifactsDirectory)/openapispec"
- runVersion: "latest"
-
- - task: CmdLine@2
- displayName: 'Download OpenApi Generator'
- inputs:
- script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
-
-## Authenticate with npm registry
- - task: npmAuthenticate@0
- inputs:
- workingFile: ./.npmrc
- customEndpoint: 'jellyfin-bot for NPM'
-
-## Generate npm api client
- - task: CmdLine@2
- displayName: 'Build stable typescript axios client'
- inputs:
- script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
-
-## Run npm install
- - task: Npm@1
- displayName: 'Install npm dependencies'
- inputs:
- command: install
- workingDir: ./apiclient/generated/typescript/axios
-
-## Publish npm packages
- - task: Npm@1
- displayName: 'Publish stable typescript axios client'
- inputs:
- command: custom
- customCommand: publish --access public
- publishRegistry: useExternalRegistry
- publishEndpoint: 'jellyfin-bot for NPM'
- workingDir: ./apiclient/generated/typescript/axios
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 20f4dfe33..543fd7fc6 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -160,7 +160,6 @@ jobs:
dependsOn:
- BuildPackage
- BuildDocker
- condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
@@ -186,9 +185,6 @@ jobs:
- job: PublishNuget
displayName: 'Publish NuGet packages'
- dependsOn:
- - BuildPackage
- condition: succeeded('BuildPackage')
pool:
vmImage: 'ubuntu-latest'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 95e0d8c58..7838b3b02 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
- targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
+ targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 6430503f9..c028b6e3e 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -61,6 +61,3 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
-
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- - template: azure-pipelines-api-client.yml
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d67e1c98b..12f1f5ed5 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -33,7 +33,13 @@ assignees: ''
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
-**Logs**
+**Server Logs**
+<!-- Please paste any log errors. -->
+
+**FFmpeg Logs**
+<!-- Please paste any log errors. -->
+
+**Browser Console Logs**
<!-- Please paste any log errors. -->
**Screenshots**
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0874cae2e..70bcd4973 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,4 +6,10 @@ updates:
interval: weekly
time: '12:00'
open-pull-requests-limit: 10
-
+
+- package-ecosystem: github-actions
+ directory: '/'
+ schedule:
+ interval: weekly
+ time: '12:00'
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
new file mode 100644
index 000000000..8da2349c8
--- /dev/null
+++ b/.github/workflows/automation.yml
@@ -0,0 +1,74 @@
+name: Automation
+
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+ issue_comment:
+
+jobs:
+ label:
+ name: Labeling
+ runs-on: ubuntu-latest
+ steps:
+ - name: Apply label
+ uses: eps1lon/actions-label-merge-conflict@v2.0.1
+ if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
+ with:
+ dirtyLabel: 'merge conflict'
+ repoToken: ${{ secrets.JF_BOT_TOKEN }}
+
+ project:
+ name: Project board
+ runs-on: ubuntu-latest
+ steps:
+ - name: Remove from 'Current Release' project
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
+ continue-on-error: true
+ with:
+ project: Current Release
+ action: delete
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add to 'Release Next' project
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
+ continue-on-error: true
+ with:
+ project: Release Next
+ column: In progress
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add to 'Current Release' project
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
+ continue-on-error: true
+ with:
+ project: Current Release
+ column: In progress
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Check number of comments from the team member
+ if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
+ id: member_comments
+ run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
+
+ - name: Move issue to needs triage
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
+ continue-on-error: true
+ with:
+ project: Issue Triage for Main Repo
+ column: Needs triage
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+ - name: Add issue to triage project
+ uses: alex-page/github-project-automation-plus@v0.7.1
+ if: github.event.issue.pull_request == '' && github.event.action == 'opened'
+ continue-on-error: true
+ with:
+ project: Issue Triage for Main Repo
+ column: Pending response
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
new file mode 100644
index 000000000..e0b91ecee
--- /dev/null
+++ b/.github/workflows/commands.yml
@@ -0,0 +1,119 @@
+name: Commands
+on:
+ issue_comment:
+ types:
+ - created
+ - edited
+ pull_request_target:
+ types:
+ - labeled
+ - synchronize
+
+jobs:
+ rebase:
+ name: Rebase
+ if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: '+1'
+
+ - name: Checkout the latest code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ fetch-depth: 0
+
+ - name: Automatic Rebase
+ uses: cirrus-actions/rebase@1.4
+ env:
+ GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+
+ check-backport:
+ name: Check Backport
+ if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: eyes
+
+ - name: Checkout the latest code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ fetch-depth: 0
+
+ - name: Notify as running
+ id: comment_running
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ issue-number: ${{ github.event.issue.number }}
+ body: |
+ Running backport tests...
+
+ - name: Perform test backport
+ id: run_tests
+ run: |
+ set +o errexit
+ git config --global user.name "Jellyfin Bot"
+ git config --global user.email "team@jellyfin.org"
+ CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
+ git checkout master
+ git merge --no-ff ${CURRENT_BRANCH}
+ MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
+ git fetch --all
+ CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
+ stable_branch="Current stable release branch: ${CURRENT_STABLE}"
+ echo ${stable_branch}
+ echo ::set-output name=branch::${stable_branch}
+ git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
+ git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
+ retcode=$?
+ cat output.txt | grep -v 'hint:'
+ output="$( grep -v 'hint:' output.txt )"
+ output="${output//'%'/'%25'}"
+ output="${output//$'\n'/'%0A'}"
+ output="${output//$'\r'/'%0D'}"
+ echo ::set-output name=output::$output
+ exit ${retcode}
+
+ - name: Notify with result success
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null && success() }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ steps.comment_running.outputs.comment-id }}
+ body: |
+ ${{ steps.run_tests.outputs.branch }}
+ Output from `git cherry-pick`:
+
+ ---
+
+ ${{ steps.run_tests.outputs.output }}
+ reactions: hooray
+
+ - name: Notify with result failure
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null && failure() }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ steps.comment_running.outputs.comment-id }}
+ body: |
+ ${{ steps.run_tests.outputs.branch }}
+ Output from `git cherry-pick`:
+
+ ---
+
+ ${{ steps.run_tests.outputs.output }}
+ reactions: confused
diff --git a/.gitignore b/.gitignore
index 7cd3d0068..252210e57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -268,6 +268,7 @@ doc/
# Deployment artifacts
dist
*.exe
+*.dll
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 89788b234..59addc7fa 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -17,6 +17,7 @@
- [bugfixin](https://github.com/bugfixin)
- [chaosinnovator](https://github.com/chaosinnovator)
- [ckcr4lyf](https://github.com/ckcr4lyf)
+ - [cocool97](https://github.com/cocool97)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [crankdoofus](https://github.com/crankdoofus)
- [crobibero](https://github.com/crobibero)
@@ -49,6 +50,7 @@
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
+ - [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
@@ -68,6 +70,7 @@
- [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro)
- [Matt07211](https://github.com/Matt07211)
+ - [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)
- [mitchfizz05](https://github.com/mitchfizz05)
- [MrTimscampi](https://github.com/MrTimscampi)
@@ -104,10 +107,11 @@
- [shemanaev](https://github.com/shemanaev)
- [skaro13](https://github.com/skaro13)
- [sl1288](https://github.com/sl1288)
+ - [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
- - [ssenart] (https://github.com/ssenart)
+ - [ssenart](https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000)
@@ -143,6 +147,7 @@
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
+ - [peterspenler](https://github.com/peterspenler)
# Emby Contributors
diff --git a/Dockerfile b/Dockerfile
index 41dd3d081..4e2d06b82 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,14 +1,14 @@
ARG DOTNET_VERSION=5.0
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
- && yarn install \
+ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index e0eaca0ed..25a0de7db 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -5,12 +5,12 @@
ARG DOTNET_VERSION=5.0
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
- && yarn install \
+ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index db7de935c..c9f19c5a3 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -5,12 +5,12 @@
ARG DOTNET_VERSION=5.0
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
- && yarn install \
+ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
index e63a85860..5ceeb5530 100644
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ b/Emby.Dlna/Configuration/DlnaOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace Emby.Dlna.Configuration
diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs
index fc02e1751..3ca43052a 100644
--- a/Emby.Dlna/ConfigurationExtension.cs
+++ b/Emby.Dlna/ConfigurationExtension.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using Emby.Dlna.Configuration;
diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs
index 2f8d197a7..1a1790ee6 100644
--- a/Emby.Dlna/ConnectionManager/ControlHandler.cs
+++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs
@@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager
}
/// <inheritdoc />
- protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
{
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
index 2f3107450..7b8c50440 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 27f1fdaba..27c5b2268 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -1,5 +1,6 @@
+#nullable disable
+
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -7,7 +8,6 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
-using Emby.Dlna.Configuration;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
@@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory
}
/// <inheritdoc />
- protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{
if (xmlWriter == null)
{
@@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory
/// <summary>
/// Adds a "XSetBookmark" element to the xml document.
/// </summary>
- /// <param name="sparams">The <see cref="IDictionary"/>.</param>
- private void HandleXSetBookmark(IDictionary<string, string> sparams)
+ /// <param name="sparams">The method parameters.</param>
+ private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams)
{
var id = sparams["ObjectID"];
@@ -306,34 +306,17 @@ namespace Emby.Dlna.ContentDirectory
}
/// <summary>
- /// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist.
- /// </summary>
- /// <param name="sparams">The <see cref="IDictionary"/>.</param>
- /// <param name="key">The key.</param>
- /// <param name="defaultValue">The defaultValue.</param>
- /// <returns>The <see cref="string"/>.</returns>
- public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue)
- {
- if (sparams != null && sparams.TryGetValue(key, out string val))
- {
- return val;
- }
-
- return defaultValue;
- }
-
- /// <summary>
/// Builds the "Browse" xml response.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
- /// <param name="sparams">The <see cref="IDictionary"/>.</param>
+ /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The device Id to use.</param>
- private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
+ private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{
var id = sparams["ObjectID"];
var flag = sparams["BrowseFlag"];
- var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
- var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
+ var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
+ var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty));
var provided = 0;
@@ -435,9 +418,9 @@ namespace Emby.Dlna.ContentDirectory
/// Builds the response to the "X_BrowseByLetter request.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
- /// <param name="sparams">The <see cref="IDictionary"/>.</param>
+ /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The device id.</param>
- private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
+ private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{
// TODO: Implement this method
HandleSearch(xmlWriter, sparams, deviceId);
@@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory
/// Builds a response to the "Search" request.
/// </summary>
/// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param>
- /// <param name="sparams">The sparams<see cref="IDictionary"/>.</param>
+ /// <param name="sparams">The method parameters.</param>
/// <param name="deviceId">The deviceId<see cref="string"/>.</param>
- private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
+ private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId)
{
- var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
- var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
- var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
+ 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
diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs
index 982ae5d68..187dc1d75 100644
--- a/Emby.Dlna/ContentDirectory/StubType.cs
+++ b/Emby.Dlna/ContentDirectory/StubType.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1602
namespace Emby.Dlna.ContentDirectory
{
diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs
index 4ea4e4e48..8ee6325e9 100644
--- a/Emby.Dlna/ControlRequest.cs
+++ b/Emby.Dlna/ControlRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs
index d827eef26..a7f2d4a73 100644
--- a/Emby.Dlna/ControlResponse.cs
+++ b/Emby.Dlna/ControlResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 8b50d47fb..2982ce97e 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -208,7 +210,8 @@ namespace Emby.Dlna.Didl
var targetWidth = streamInfo.TargetWidth;
var targetHeight = streamInfo.TargetHeight;
- var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
+ var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
+ _profile,
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
@@ -599,7 +602,8 @@ namespace Emby.Dlna.Didl
? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType;
- var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
+ var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
+ _profile,
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetAudioBitrate,
@@ -974,15 +978,28 @@ namespace Emby.Dlna.Didl
return;
}
- var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
+ // TODO: Remove these default values
+ var albumArtUrlInfo = GetImageUrl(
+ imageInfo,
+ _profile.MaxAlbumArtWidth ?? 10000,
+ _profile.MaxAlbumArtHeight ?? 10000,
+ "jpg");
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
- writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
- writer.WriteString(albumartUrlInfo.url);
+ if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
+ {
+ writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
+ }
+
+ writer.WriteString(albumArtUrlInfo.url);
writer.WriteFullEndElement();
- // TOOD: Remove these default values
- var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
+ // 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)
@@ -1033,8 +1050,7 @@ namespace Emby.Dlna.Didl
var width = albumartUrlInfo.width ?? maxWidth;
var height = albumartUrlInfo.height ?? maxHeight;
- var contentFeatures = new ContentFeatureBuilder(_profile)
- .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
+ var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
writer.WriteAttributeString(
"protocolInfo",
@@ -1206,8 +1222,7 @@ namespace Emby.Dlna.Didl
if (width.HasValue && height.HasValue)
{
- var newSize = DrawingUtils.Resize(
- new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
+ var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
width = newSize.Width;
height = newSize.Height;
diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs
index 2b86ea333..b66f53ece 100644
--- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs
+++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs
@@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
{
public class StringWriterWithEncoding : StringWriter
{
- private readonly Encoding _encoding;
+ private readonly Encoding? _encoding;
public StringWriterWithEncoding()
{
diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs
index 4c6ca869a..6cc6b73a0 100644
--- a/Emby.Dlna/DlnaConfigurationFactory.cs
+++ b/Emby.Dlna/DlnaConfigurationFactory.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 21ba1c755..a1b106704 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -36,7 +38,7 @@ namespace Emby.Dlna
private readonly ILogger<DlnaManager> _logger;
private readonly IServerApplicationHost _appHost;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@@ -111,7 +113,7 @@ namespace Emby.Dlna
if (profile != null)
{
- _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+ _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
}
else
{
@@ -126,92 +128,57 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
- builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
- builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
- builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
- builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
- builder.Append("ModelName:").AppendLine(profile.ModelName);
- builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
- builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
- builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
+ builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
+ builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
+ builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
+ builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
+ builder.Append("ModelName: ").AppendLine(profile.ModelName);
+ builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
+ builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
+ builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
- private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
+ /// <summary>
+ /// Attempts to match a device with a profile.
+ /// Rules:
+ /// - If the profile field has no value, the field matches irregardless 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)
{
- if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
- {
- if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
- {
- if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
- {
- if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
- {
- if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ModelName))
- {
- if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
- {
- return false;
- }
- }
-
- if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
- {
- if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
- {
- return false;
- }
- }
+ 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);
+ }
- if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
+ private bool IsRegexOrSubstringMatch(string input, string pattern)
+ {
+ if (string.IsNullOrEmpty(pattern))
{
- if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
- {
- return false;
- }
+ // In profile identification: An empty pattern matches anything.
+ return true;
}
- if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
+ if (string.IsNullOrEmpty(input))
{
- if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
- {
- return false;
- }
+ // The profile contains a value, and the device doesn't.
+ return false;
}
- return true;
- }
-
- private bool IsRegexOrSubstringMatch(string input, string pattern)
- {
try
{
- return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+ return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
+ || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
catch (ArgumentException ex)
{
@@ -333,7 +300,12 @@ namespace Emby.Dlna
throw new ArgumentNullException(nameof(id));
}
- var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+ var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+
+ if (info == null)
+ {
+ return null;
+ }
return ParseProfileFile(info.Path, info.Info.Type);
}
@@ -395,7 +367,8 @@ namespace Emby.Dlna
{
Directory.CreateDirectory(systemProfilesPath);
- using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -553,7 +526,7 @@ namespace Emby.Dlna
private void DumpProfiles()
{
- DeviceProfile[] list = new []
+ DeviceProfile[] list = new[]
{
new SamsungSmartTvProfile(),
new XboxOneProfile(),
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 8b057a095..a40578e40 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -21,11 +21,11 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs
index 1b1bd426c..8c82dcbf6 100644
--- a/Emby.Dlna/EventSubscriptionResponse.cs
+++ b/Emby.Dlna/EventSubscriptionResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index ff81e83b5..2e672b886 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs
index 40d73ee0e..4fd7f8169 100644
--- a/Emby.Dlna/Eventing/EventSubscription.cs
+++ b/Emby.Dlna/Eventing/EventSubscription.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 82490ec31..0309926ab 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -5,7 +7,6 @@ using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
-using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
@@ -128,7 +129,8 @@ namespace Emby.Dlna.Main
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
- if (_disabled)
+
+ if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
@@ -228,7 +230,10 @@ namespace Emby.Dlna.Main
{
try
{
- ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ if (communicationsServer != null)
+ {
+ ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ }
}
catch (Exception ex)
{
@@ -313,9 +318,12 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
- // DLNA will only work over http, so we must reset to http:// : {port}
- uri.Scheme = "http://";
- uri.Port = _netConfig.HttpServerPortNumber;
+ if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
+ {
+ // DLNA will only work over http, so we must reset to http:// : {port}.
+ uri.Scheme = "http";
+ uri.Port = _netConfig.HttpServerPortNumber;
+ }
var device = new SsdpRootDevice
{
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
index 464f71a6f..d8fb12742 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
@@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
}
/// <inheritdoc />
- protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter)
+ protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
{
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
{
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
index 37840cd09..f3789a791 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using Emby.Dlna.Common;
using Emby.Dlna.Service;
-using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 938ce5fbf..6c580d15b 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
+ var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null)
{
return false;
@@ -235,7 +237,13 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("Setting mute");
var value = mute ? 1 : 0;
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ rendererCommands.BuildPost(command, service.ServiceType, value),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
IsMuted = mute;
@@ -253,7 +261,7 @@ namespace Emby.Dlna.PlayTo
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
+ var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null)
{
return;
@@ -270,7 +278,13 @@ namespace Emby.Dlna.PlayTo
// Remote control will perform better
Volume = value;
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ rendererCommands.BuildPost(command, service.ServiceType, value),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
@@ -278,7 +292,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null)
{
return;
@@ -291,7 +305,13 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
- await new SsdpHttpClient(_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"))
+ await new SsdpHttpClient(_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"),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
@@ -305,7 +325,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
if (command == null)
{
return;
@@ -325,14 +345,21 @@ namespace Emby.Dlna.PlayTo
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ post,
+ header: header,
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
- await Task.Delay(50).ConfigureAwait(false);
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
try
{
- await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
+ await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
}
catch
{
@@ -343,6 +370,42 @@ namespace Emby.Dlna.PlayTo
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 == null)
+ {
+ return;
+ }
+
+ var dictionary = new Dictionary<string, string>
+ {
+ { "NextURI", url },
+ { "NextURIMetaData", CreateDidlMeta(metaData) }
+ };
+
+ var service = GetAvTransportService();
+
+ if (service == null)
+ {
+ throw new InvalidOperationException("Unable to find service");
+ }
+
+ var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
private static string CreateDidlMeta(string value)
{
if (string.IsNullOrEmpty(value))
@@ -378,6 +441,10 @@ namespace Emby.Dlna.PlayTo
public async Task SetPlay(CancellationToken cancellationToken)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
+ if (avCommands == null)
+ {
+ return;
+ }
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
@@ -388,7 +455,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
if (command == null)
{
return;
@@ -396,7 +463,13 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ avCommands.BuildPost(command, service.ServiceType, 1),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
@@ -406,7 +479,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
if (command == null)
{
return;
@@ -414,7 +487,13 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+ await new SsdpHttpClient(_httpClientFactory)
+ .SendCommandAsync(
+ Properties.BaseUrl,
+ service,
+ command.Name,
+ avCommands.BuildPost(command, service.ServiceType, 1),
+ cancellationToken: cancellationToken)
.ConfigureAwait(false);
TransportState = TransportState.Paused;
@@ -528,7 +607,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
+ var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
if (command == null)
{
return;
@@ -578,7 +657,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
- var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
+ var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
if (command == null)
{
return;
@@ -665,6 +744,10 @@ namespace Emby.Dlna.PlayTo
}
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
+ if (rendererCommands == null)
+ {
+ return null;
+ }
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
@@ -733,6 +816,11 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
+ if (rendererCommands == null)
+ {
+ return (false, null);
+ }
+
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
@@ -914,6 +1002,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
+ if (document == null)
+ {
+ return null;
+ }
AvCommands = TransportCommands.Create(document);
return AvCommands;
@@ -942,6 +1034,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
+ if (document == null)
+ {
+ return null;
+ }
RendererCommands = TransportCommands.Create(document);
return RendererCommands;
@@ -973,6 +1069,10 @@ namespace Emby.Dlna.PlayTo
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
+ if (document == null)
+ {
+ return null;
+ }
var friendlyNames = new List<string>();
@@ -990,7 +1090,7 @@ namespace Emby.Dlna.PlayTo
var deviceProperties = new DeviceInfo()
{
- Name = string.Join(" ", friendlyNames),
+ Name = string.Join(' ', friendlyNames),
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
};
diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs
index d3daab9e0..2acfff4eb 100644
--- a/Emby.Dlna/PlayTo/DeviceInfo.cs
+++ b/Emby.Dlna/PlayTo/DeviceInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
index dabd079af..2bc4d8cc2 100644
--- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
+++ b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 315be1e8b..0e49fd2c0 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -102,6 +104,22 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
+ /*
+ * 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
@@ -132,7 +150,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{
- if (_disposed)
+ if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
return;
}
@@ -156,6 +174,15 @@ namespace Emby.Dlna.PlayTo
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 == streamInfo.ItemId);
+ if (currentItemIndex >= 0)
+ {
+ _currentPlaylistIndex = currentItemIndex;
+ }
+
+ await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
}
catch (Exception ex)
{
@@ -425,6 +452,11 @@ namespace Emby.Dlna.PlayTo
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);
+
return;
}
@@ -499,8 +531,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Audio)
{
- return new ContentFeatureBuilder(profile)
- .BuildAudioHeader(
+ return ContentFeatureBuilder.BuildAudioHeader(
+ profile,
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate,
@@ -514,8 +546,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Video)
{
- var list = new ContentFeatureBuilder(profile)
- .BuildVideoHeader(
+ var list = ContentFeatureBuilder.BuildVideoHeader(
+ profile,
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
@@ -623,6 +655,9 @@ namespace Emby.Dlna.PlayTo
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);
+
var streamInfo = currentitem.StreamInfo;
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
{
@@ -736,6 +771,10 @@ namespace Emby.Dlna.PlayTo
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);
+
if (EnableClientSideSeek(newItem.StreamInfo))
{
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@@ -761,6 +800,10 @@ namespace Emby.Dlna.PlayTo
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);
+
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
{
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@@ -777,7 +820,7 @@ namespace Emby.Dlna.PlayTo
var currentWait = 0;
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
{
- await Task.Delay(Interval).ConfigureAwait(false);
+ await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
currentWait += Interval;
}
@@ -943,11 +986,7 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
-
- // Be careful, IsDirectStream==true by default (Static != false or not in query).
- // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
- request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
-
+ request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index a6793a708..35bf5927c 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo
if (controller == null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
+ if (device == 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.LocalIpAddress);
+ string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
controller = new PlayToController(
sessionInfo,
diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
index d14617c8a..c7d2b28df 100644
--- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
index 3f8d55263..f8a14f411 100644
--- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
index deeb47918..6661f92ac 100644
--- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs
index 85846166c..5056e69ae 100644
--- a/Emby.Dlna/PlayTo/PlaylistItem.cs
+++ b/Emby.Dlna/PlayTo/PlaylistItem.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.Dlna;
diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
index e28840a89..657491303 100644
--- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
+++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index 557bc69a7..f14f73bb6 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -1,8 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
-using System.IO;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
@@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo
cancellationToken)
.ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var reader = new StreamReader(stream, Encoding.UTF8);
- return XDocument.Parse(
- await reader.ReadToEndAsync().ConfigureAwait(false),
- LoadOptions.PreserveWhitespace);
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.PreserveWhitespace,
+ cancellationToken).ConfigureAwait(false);
}
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var reader = new StreamReader(stream, Encoding.UTF8);
- return XDocument.Parse(
- await reader.ReadToEndAsync().ConfigureAwait(false),
- LoadOptions.PreserveWhitespace);
+ try
+ {
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.PreserveWhitespace,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ return null;
+ }
}
private async Task<HttpResponseMessage> PostSoapDataAsync(
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
index 0865968ad..b58669355 100644
--- a/Emby.Dlna/PlayTo/TransportCommands.cs
+++ b/Emby.Dlna/PlayTo/TransportCommands.cs
@@ -13,12 +13,10 @@ 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>";
- private List<StateVariable> _stateVariables = new List<StateVariable>();
- private List<ServiceAction> _serviceActions = new List<ServiceAction>();
- public List<StateVariable> StateVariables => _stateVariables;
+ public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
- public List<ServiceAction> ServiceActions => _serviceActions;
+ public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
public static TransportCommands Create(XDocument document)
{
@@ -48,7 +46,7 @@ namespace Emby.Dlna.PlayTo
{
var serviceAction = new ServiceAction
{
- Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+ Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
};
var argumentList = serviceAction.ArgumentList;
@@ -70,9 +68,9 @@ namespace Emby.Dlna.PlayTo
return new Argument
{
- Name = container.GetValue(UPnpNamespaces.Svc + "name"),
- Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
- RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
+ Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
+ Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
+ RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
};
}
@@ -91,8 +89,8 @@ namespace Emby.Dlna.PlayTo
return new StateVariable
{
- Name = container.GetValue(UPnpNamespaces.Svc + "name"),
- DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
+ Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
+ DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
AllowedValues = allowedValues
};
}
@@ -168,7 +166,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
- private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
+ private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
{
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs
index 7068a5d24..2058e9dc7 100644
--- a/Emby.Dlna/PlayTo/TransportState.cs
+++ b/Emby.Dlna/PlayTo/TransportState.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1602
namespace Emby.Dlna.PlayTo
{
diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs
index 0d9478e42..02d2da58d 100644
--- a/Emby.Dlna/PlayTo/uBaseObject.cs
+++ b/Emby.Dlna/PlayTo/uBaseObject.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs
index d4af72b62..8eaf12ba9 100644
--- a/Emby.Dlna/Profiles/DefaultProfile.cs
+++ b/Emby.Dlna/Profiles/DefaultProfile.cs
@@ -1,5 +1,7 @@
#pragma warning disable CS1591
+using System;
+using System.Globalization;
using System.Linq;
using MediaBrowser.Model.Dlna;
@@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
{
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:*";
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 09525aae4..3f3dfccd3 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -250,7 +250,8 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
- return SecurityElement.Escape(url);
+ // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
+ return SecurityElement.Escape(url) ?? string.Empty;
}
private IEnumerable<DeviceIcon> GetIcons()
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 8d2486fee..904c23d99 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -47,7 +47,7 @@ namespace Emby.Dlna.Service
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
{
- ControlRequestInfo requestInfo = null;
+ ControlRequestInfo? requestInfo = null;
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{
@@ -151,7 +151,7 @@ namespace Emby.Dlna.Service
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{
- string namespaceURI = null, localName = null;
+ string? namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false);
@@ -210,7 +210,7 @@ namespace Emby.Dlna.Service
}
}
- protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
+ protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter);
private void LogRequest(ControlRequest request)
{
diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
index 8c7d961f3..391dda147 100644
--- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs
+++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp
{
lock (_syncLock)
{
- if (_listenerCount > 0 && _deviceLocator == null)
+ if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
{
_deviceLocator = new SsdpDeviceLocator(_commsServer);
@@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
{
Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers,
- LocalIpAddress = e.LocalIpAddress
+ RemoteIpAddress = e.RemoteIpAddress
});
DeviceDiscoveredInternal?.Invoke(this, args);
diff --git a/Emby.Dlna/Ssdp/SsdpExtensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs
index e7a52f168..d00eb02b4 100644
--- a/Emby.Dlna/Ssdp/SsdpExtensions.cs
+++ b/Emby.Dlna/Ssdp/SsdpExtensions.cs
@@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
{
public static class SsdpExtensions
{
- public static string GetValue(this XElement container, XName name)
+ 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)
+ 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)
+ public static string? GetDescendantValue(this XElement container, XName name)
=> container.Descendants(name).FirstOrDefault()?.Value;
}
}
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index 7d479a5c6..5c5afe1c6 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -25,7 +25,6 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 8a2301d2d..7d952aa23 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
@@ -171,21 +172,31 @@ namespace Emby.Drawing
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
- ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
- string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
+ string cacheFilePath = GetCacheFilePath(
+ originalImagePath,
+ options.Width,
+ options.Height,
+ options.MaxWidth,
+ options.MaxHeight,
+ options.FillWidth,
+ options.FillHeight,
+ quality,
+ dateModified,
+ outputFormat,
+ options.AddPlayedIndicator,
+ options.PercentPlayed,
+ options.UnplayedCount,
+ options.Blur,
+ options.BackgroundColor,
+ options.ForegroundLayer);
try
{
if (!File.Exists(cacheFilePath))
{
- if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
- {
- options.CropWhiteSpace = false;
- }
-
string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
@@ -246,48 +257,111 @@ namespace Emby.Drawing
/// <summary>
/// Gets the cache file path based on a set of parameters.
/// </summary>
- private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
+ private string GetCacheFilePath(
+ string originalPath,
+ int? width,
+ int? height,
+ int? maxWidth,
+ int? maxHeight,
+ int? fillWidth,
+ int? fillHeight,
+ int quality,
+ DateTime dateModified,
+ ImageFormat format,
+ bool addPlayedIndicator,
+ double percentPlayed,
+ int? unwatchedCount,
+ int? blur,
+ string backgroundColor,
+ string foregroundLayer)
{
- var filename = originalPath
- + "width=" + outputSize.Width
- + "height=" + outputSize.Height
- + "quality=" + quality
- + "datemodified=" + dateModified.Ticks
- + "f=" + format;
+ var filename = new StringBuilder(256);
+ filename.Append(originalPath);
+
+ filename.Append(",quality=");
+ filename.Append(quality);
+
+ filename.Append(",datemodified=");
+ filename.Append(dateModified.Ticks);
+
+ filename.Append(",f=");
+ filename.Append(format);
+
+ if (width.HasValue)
+ {
+ filename.Append(",width=");
+ filename.Append(width.Value);
+ }
+
+ if (height.HasValue)
+ {
+ filename.Append(",height=");
+ filename.Append(height.Value);
+ }
+
+ if (maxWidth.HasValue)
+ {
+ filename.Append(",maxwidth=");
+ filename.Append(maxWidth.Value);
+ }
+
+ if (maxHeight.HasValue)
+ {
+ filename.Append(",maxheight=");
+ filename.Append(maxHeight.Value);
+ }
+
+ if (fillWidth.HasValue)
+ {
+ filename.Append(",fillwidth=");
+ filename.Append(fillWidth.Value);
+ }
+
+ if (fillHeight.HasValue)
+ {
+ filename.Append(",fillheight=");
+ filename.Append(fillHeight.Value);
+ }
if (addPlayedIndicator)
{
- filename += "pl=true";
+ filename.Append(",pl=true");
}
if (percentPlayed > 0)
{
- filename += "p=" + percentPlayed;
+ filename.Append(",p=");
+ filename.Append(percentPlayed);
}
if (unwatchedCount.HasValue)
{
- filename += "p=" + unwatchedCount.Value;
+ filename.Append(",p=");
+ filename.Append(unwatchedCount.Value);
}
if (blur.HasValue)
{
- filename += "blur=" + blur.Value;
+ filename.Append(",blur=");
+ filename.Append(blur.Value);
}
if (!string.IsNullOrEmpty(backgroundColor))
{
- filename += "b=" + backgroundColor;
+ filename.Append(",b=");
+ filename.Append(backgroundColor);
}
if (!string.IsNullOrEmpty(foregroundLayer))
{
- filename += "fl=" + foregroundLayer;
+ filename.Append(",fl=");
+ filename.Append(foregroundLayer);
}
- filename += "v=" + Version;
+ filename.Append(",v=");
+ filename.Append(Version);
- return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
+ return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
}
/// <inheritdoc />
@@ -352,8 +426,13 @@ namespace Emby.Drawing
}
/// <inheritdoc />
- public string GetImageCacheTag(User user)
+ public string? GetImageCacheTag(User user)
{
+ if (user.ProfileImage == null)
+ {
+ return null;
+ }
+
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
.ToString("N", CultureInfo.InvariantCulture);
}
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs
index 2a1cfd3da..1c05aa916 100644
--- a/Emby.Drawing/NullImageEncoder.cs
+++ b/Emby.Drawing/NullImageEncoder.cs
@@ -32,7 +32,7 @@ namespace Emby.Drawing
=> throw new NotImplementedException();
/// <inheritdoc />
- public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
+ public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
{
throw new NotImplementedException();
}
diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs
index 8b47dd12e..af4aa0059 100644
--- a/Emby.Naming/Audio/AudioFileParser.cs
+++ b/Emby.Naming/Audio/AudioFileParser.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
namespace Emby.Naming.Audio
{
@@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
/// <returns>True if file at path is audio file.</returns>
public static bool IsAudioFile(string path, NamingOptions options)
{
- var extension = Path.GetExtension(path);
- return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ var extension = Path.GetExtension(path.AsSpan());
+ return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index e9ea9b7a5..ca5322890 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
- var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
+ var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
foreach (var group in groupedBy)
{
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index b43203e9d..3224ff412 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -23,11 +23,12 @@
</PropertyGroup>
<ItemGroup>
- <Compile Include="..\SharedVersion.cs" />
+ <Compile Include="../SharedVersion.cs" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+ <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup>
<PropertyGroup>
@@ -44,7 +45,6 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index f7df58786..5e952e47b 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -16,7 +16,7 @@ namespace Emby.Naming.TV
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
+ /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
public EpisodeResolver(NamingOptions options)
{
_options = options;
@@ -62,12 +62,16 @@ namespace Emby.Naming.TV
container = extension.TrimStart('.');
}
- var flags = new FlagParser(_options).GetFlags(path);
- var format3DResult = new Format3DParser(_options).Parse(flags);
+ var format3DResult = Format3DParser.Parse(path, _options);
var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
+ if (!parsingResult.Success && !isStub)
+ {
+ return null;
+ }
+
return new EpisodeInfo(path)
{
Container = container,
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index 09a0cd189..4eef3ebc5 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Emby.Naming.Video
@@ -16,8 +17,14 @@ namespace Emby.Naming.Video
/// <param name="expressions">List of regex to parse name and year from.</param>
/// <param name="newName">Parsing result string.</param>
/// <returns>True if parsing was successful.</returns>
- public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
+ public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
{
+ if (string.IsNullOrEmpty(name))
+ {
+ newName = ReadOnlySpan<char>.Empty;
+ return false;
+ }
+
var len = expressions.Count;
for (int i = 0; i < len; i++)
{
@@ -41,7 +48,7 @@ namespace Emby.Naming.Video
return true;
}
- newName = string.Empty;
+ newName = ReadOnlySpan<char>.Empty;
return false;
}
}
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index 1d3b36a1a..1fade985b 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -30,69 +30,72 @@ namespace Emby.Naming.Video
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public ExtraResult GetExtraInfo(string path)
{
- return _options.VideoExtraRules
- .Select(i => GetExtraInfo(path, i))
- .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
- }
-
- private ExtraResult GetExtraInfo(string path, ExtraRule rule)
- {
var result = new ExtraResult();
- if (rule.MediaType == MediaType.Audio)
+ for (var i = 0; i < _options.VideoExtraRules.Length; i++)
{
- if (!AudioFileParser.IsAudioFile(path, _options))
+ var rule = _options.VideoExtraRules[i];
+ if (rule.MediaType == MediaType.Audio)
{
- return result;
+ if (!AudioFileParser.IsAudioFile(path, _options))
+ {
+ continue;
+ }
}
- }
- else if (rule.MediaType == MediaType.Video)
- {
- if (!new VideoResolver(_options).IsVideoFile(path))
+ else if (rule.MediaType == MediaType.Video)
{
- return result;
+ if (!VideoResolver.IsVideoFile(path, _options))
+ {
+ continue;
+ }
}
- }
-
- if (rule.RuleType == ExtraRuleType.Filename)
- {
- var filename = Path.GetFileNameWithoutExtension(path);
- if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
+ var pathSpan = path.AsSpan();
+ if (rule.RuleType == ExtraRuleType.Filename)
{
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
- }
- }
- else if (rule.RuleType == ExtraRuleType.Suffix)
- {
- var filename = Path.GetFileNameWithoutExtension(path);
+ var filename = Path.GetFileNameWithoutExtension(pathSpan);
- if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
+ if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
+ }
+ else if (rule.RuleType == ExtraRuleType.Suffix)
{
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
+ var filename = Path.GetFileNameWithoutExtension(pathSpan);
+
+ if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
}
- }
- else if (rule.RuleType == ExtraRuleType.Regex)
- {
- var filename = Path.GetFileName(path);
+ else if (rule.RuleType == ExtraRuleType.Regex)
+ {
+ var filename = Path.GetFileName(path);
- var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+ var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
- if (regex.IsMatch(filename))
+ if (regex.IsMatch(filename))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
+ }
+ else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
+ var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
+ if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+ {
+ result.ExtraType = rule.ExtraType;
+ result.Rule = rule;
+ }
}
- }
- else if (rule.RuleType == ExtraRuleType.DirectoryName)
- {
- var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
- if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
+
+ if (result.ExtraType != null)
{
- result.ExtraType = rule.ExtraType;
- result.Rule = rule;
+ return result;
}
}
diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs
deleted file mode 100644
index 439de1813..000000000
--- a/Emby.Naming/Video/FlagParser.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.IO;
-using Emby.Naming.Common;
-
-namespace Emby.Naming.Video
-{
- /// <summary>
- /// Parses list of flags from filename based on delimiters.
- /// </summary>
- public class FlagParser
- {
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FlagParser"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
- public FlagParser(NamingOptions options)
- {
- _options = options;
- }
-
- /// <summary>
- /// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
- /// </summary>
- /// <param name="path">Path to file.</param>
- /// <returns>List of found flags.</returns>
- public string[] GetFlags(string path)
- {
- return GetFlags(path, _options.VideoFlagDelimiters);
- }
-
- /// <summary>
- /// Parses flags from filename based on delimiters.
- /// </summary>
- /// <param name="path">Path to file.</param>
- /// <param name="delimiters">Delimiters used to extract flags.</param>
- /// <returns>List of found flags.</returns>
- public string[] GetFlags(string path, char[] delimiters)
- {
- if (string.IsNullOrEmpty(path))
- {
- return Array.Empty<string>();
- }
-
- // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
-
- var file = Path.GetFileName(path);
-
- return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 4fd5d78ba..089089989 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/Emby.Naming/Video/Format3DParser.cs
@@ -1,45 +1,37 @@
using System;
-using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
- /// Parste 3D format related flags.
+ /// Parse 3D format related flags.
/// </summary>
- public class Format3DParser
+ public static class Format3DParser
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Format3DParser"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
- public Format3DParser(NamingOptions options)
- {
- _options = options;
- }
+ // Static default result to save on allocation costs.
+ private static readonly Format3DResult _defaultResult = new (false, null);
/// <summary>
/// Parse 3D format related flags.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
- public Format3DResult Parse(string path)
+ public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions)
{
- int oldLen = _options.VideoFlagDelimiters.Length;
- var delimiters = new char[oldLen + 1];
- _options.VideoFlagDelimiters.CopyTo(delimiters, 0);
+ int oldLen = namingOptions.VideoFlagDelimiters.Length;
+ Span<char> delimiters = stackalloc char[oldLen + 1];
+ namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
delimiters[oldLen] = ' ';
- return Parse(new FlagParser(_options).GetFlags(path, delimiters));
+ return Parse(path, delimiters, namingOptions);
}
- internal Format3DResult Parse(string[] videoFlags)
+ private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions)
{
- foreach (var rule in _options.Format3DRules)
+ foreach (var rule in namingOptions.Format3DRules)
{
- var result = Parse(videoFlags, rule);
+ var result = Parse(path, rule, delimiters);
if (result.Is3D)
{
@@ -47,51 +39,43 @@ namespace Emby.Naming.Video
}
}
- return new Format3DResult();
+ return _defaultResult;
}
- private static Format3DResult Parse(string[] videoFlags, Format3DRule rule)
+ private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters)
{
- var result = new Format3DResult();
+ bool is3D = false;
+ string? format3D = null;
- if (string.IsNullOrEmpty(rule.PrecedingToken))
+ // If there's no preceding token we just consider it found
+ var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
+ while (path.Length > 0)
{
- result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
- result.Is3D = !string.IsNullOrEmpty(result.Format3D);
-
- if (result.Is3D)
+ var index = path.IndexOfAny(delimiters);
+ if (index == -1)
{
- result.Tokens.Add(rule.Token);
+ index = path.Length - 1;
}
- }
- else
- {
- var foundPrefix = false;
- string? format = null;
- foreach (var flag in videoFlags)
- {
- if (foundPrefix)
- {
- result.Tokens.Add(rule.PrecedingToken);
+ var currentSlice = path[..index];
+ path = path[(index + 1)..];
- if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
- {
- format = flag;
- result.Tokens.Add(rule.Token);
- }
+ if (!foundPrefix)
+ {
+ foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+ continue;
+ }
- break;
- }
+ is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
- foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+ if (is3D)
+ {
+ format3D = rule.Token;
+ break;
}
-
- result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
- result.Format3D = format;
}
- return result;
+ return is3D ? new Format3DResult(true, format3D) : _defaultResult;
}
}
}
diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs
index ac935f203..aac959c13 100644
--- a/Emby.Naming/Video/Format3DResult.cs
+++ b/Emby.Naming/Video/Format3DResult.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace Emby.Naming.Video
{
/// <summary>
@@ -10,27 +8,24 @@ namespace Emby.Naming.Video
/// <summary>
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
/// </summary>
- public Format3DResult()
+ /// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param>
+ /// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param>
+ public Format3DResult(bool is3D, string? format3D)
{
- Tokens = new List<string>();
+ Is3D = is3D;
+ Format3D = format3D;
}
/// <summary>
- /// Gets or sets a value indicating whether [is3 d].
+ /// Gets a value indicating whether [is3 d].
/// </summary>
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
- public bool Is3D { get; set; }
+ public bool Is3D { get; }
/// <summary>
- /// Gets or sets the format3 d.
+ /// Gets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
- public string? Format3D { get; set; }
-
- /// <summary>
- /// Gets or sets the tokens.
- /// </summary>
- /// <value>The tokens.</value>
- public List<string> Tokens { get; set; }
+ public string? Format3D { get; }
}
}
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 550c42961..36f65a562 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -85,10 +85,8 @@ namespace Emby.Naming.Video
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{
- var resolver = new VideoResolver(_options);
-
var list = files
- .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
+ .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
.OrderBy(i => i.FullName)
.ToList();
diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs
index 1457db737..481773ff6 100644
--- a/Emby.Naming/Video/VideoFileInfo.cs
+++ b/Emby.Naming/Video/VideoFileInfo.cs
@@ -1,3 +1,4 @@
+using System;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
@@ -106,9 +107,9 @@ namespace Emby.Naming.Video
/// Gets the file name without extension.
/// </summary>
/// <value>The file name without extension.</value>
- public string FileNameWithoutExtension => !IsDirectory
- ? System.IO.Path.GetFileNameWithoutExtension(Path)
- : System.IO.Path.GetFileName(Path);
+ public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory
+ ? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
+ : System.IO.Path.GetFileName(Path.AsSpan());
/// <inheritdoc />
public override string ToString()
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 09a030d2d..7da2dcd7a 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -12,31 +12,19 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
- public class VideoListResolver
+ public static class VideoListResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="VideoListResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
- public VideoListResolver(NamingOptions options)
- {
- _options = options;
- }
-
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="files">List of related video files.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
- public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
+ public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
{
- var videoResolver = new VideoResolver(_options);
-
var videoInfos = files
- .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
.OfType<VideoFileInfo>()
.ToList();
@@ -46,7 +34,7 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = new StackResolver(_options)
+ var stackResult = new StackResolver(namingOptions)
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos
@@ -59,23 +47,17 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
- var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
-
- var extras = GetExtras(remainingFiles, extraBaseNames);
+ var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
if (extras.Count > 0)
{
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
info.Extras = extras;
}
@@ -88,15 +70,12 @@ namespace Emby.Naming.Video
foreach (var media in standaloneMedia)
{
- var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } };
+ var info = new VideoInfo(media.Name) { Files = new[] { media } };
info.Year = info.Files[0].Year;
- var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
-
- remainingFiles = remainingFiles
- .Except(extras.Concat(new[] { media }))
- .ToList();
+ remainingFiles.Remove(media);
+ var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
info.Extras = extras;
@@ -105,8 +84,7 @@ namespace Emby.Naming.Video
if (supportMultiVersion)
{
- list = GetVideosGroupedByVersion(list)
- .ToList();
+ list = GetVideosGroupedByVersion(list, namingOptions);
}
// If there's only one resolved video, use the folder name as well to find extras
@@ -114,19 +92,14 @@ namespace Emby.Naming.Video
{
var info = list[0];
var videoPath = list[0].Files[0].Path;
- var parentPath = Path.GetDirectoryName(videoPath);
+ var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
- if (!string.IsNullOrEmpty(parentPath))
+ if (!parentPath.IsEmpty)
{
var folderName = Path.GetFileName(parentPath);
- if (!string.IsNullOrEmpty(folderName))
+ if (!folderName.IsEmpty)
{
- var extras = GetExtras(remainingFiles, new List<string> { folderName });
-
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
+ var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
extras.AddRange(info.Extras);
info.Extras = extras;
}
@@ -164,95 +137,168 @@ namespace Emby.Naming.Video
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
- Files = new List<VideoFileInfo> { i },
+ Files = new[] { i },
Year = i.Year
}));
return list;
}
- private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
+ private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
{
if (videos.Count == 0)
{
return videos;
}
- var list = new List<VideoInfo>();
-
- var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
+ var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
- if (!string.IsNullOrEmpty(folderName)
- && folderName.Length > 1
- && videos.All(i => i.Files.Count == 1
- && IsEligibleForMultiVersion(folderName, i.Files[0].Path))
- && HaveSameYear(videos))
+ if (folderName.Length <= 1 || !HaveSameYear(videos))
{
- var ordered = videos.OrderBy(i => i.Name).ToList();
-
- list.Add(ordered[0]);
+ return videos;
+ }
- var alternateVersionsLen = ordered.Count - 1;
- var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- for (int i = 0; i < alternateVersionsLen; i++)
+ // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
+ for (var i = 0; i < videos.Count; i++)
+ {
+ var video = videos[i];
+ if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
- alternateVersions[i] = ordered[i + 1].Files[0];
+ return videos;
}
+ }
+
+ // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
+ videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
- list[0].AlternateVersions = alternateVersions;
- list[0].Name = folderName;
- var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
- extras.AddRange(list[0].Extras);
- list[0].Extras = extras;
+ var list = new List<VideoInfo>
+ {
+ videos[0]
+ };
- return list;
+ var alternateVersionsLen = videos.Count - 1;
+ var alternateVersions = new VideoFileInfo[alternateVersionsLen];
+ var extras = new List<VideoFileInfo>(list[0].Extras);
+ for (int i = 0; i < alternateVersionsLen; i++)
+ {
+ var video = videos[i + 1];
+ alternateVersions[i] = video.Files[0];
+ extras.AddRange(video.Extras);
}
- return videos;
- }
+ list[0].AlternateVersions = alternateVersions;
+ list[0].Name = folderName.ToString();
+ list[0].Extras = extras;
- private bool HaveSameYear(List<VideoInfo> videos)
- {
- return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
+ return list;
}
- private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
+ private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
{
- string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
- if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+ if (videos.Count == 1)
{
- if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
- {
- testFilename = cleanName.ToString();
- }
+ return true;
+ }
- if (folderName.Length <= testFilename.Length)
+ var firstYear = videos[0].Year ?? -1;
+ for (var i = 1; i < videos.Count; i++)
+ {
+ if ((videos[i].Year ?? -1) != firstYear)
{
- testFilename = testFilename.Substring(folderName.Length).Trim();
+ return false;
}
+ }
- return string.IsNullOrEmpty(testFilename)
+ return true;
+ }
+
+ private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
+ {
+ var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
+ if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Remove the folder name before cleaning as we don't care about cleaning that part
+ if (folderName.Length <= testFilename.Length)
+ {
+ testFilename = testFilename[folderName.Length..].Trim();
+ }
+
+ // There are no span overloads for regex unfortunately
+ var tmpTestFilename = testFilename.ToString();
+ if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
+ {
+ tmpTestFilename = cleanName.Trim().ToString();
+ }
+
+ // The CleanStringParser should have removed common keywords etc.
+ return string.IsNullOrEmpty(tmpTestFilename)
|| testFilename[0] == '-'
- || testFilename[0] == '_'
- || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
+ || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+ }
+
+ private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
+ }
+
+ private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
+ {
+ if (baseName.IsEmpty)
+ {
+ return false;
}
- return false;
+ return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
+ || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ /// <summary>
+ /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
+ /// </summary>
+ /// <param name="remainingFiles">The list of remaining filenames.</param>
+ /// <param name="baseName">The base name to use for the comparison.</param>
+ /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+ /// <returns>A list of video extras for [baseName].</returns>
+ private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
}
- private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
+ /// <summary>
+ /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
+ /// </summary>
+ /// <param name="remainingFiles">The list of remaining filenames.</param>
+ /// <param name="firstBaseName">The first base name to use for the comparison.</param>
+ /// <param name="secondBaseName">The second base name to use for the comparison.</param>
+ /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+ /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
+ private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
{
- foreach (var name in baseNames.ToList())
+ var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
+ var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
+
+ var result = new List<VideoFileInfo>();
+ for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
{
- var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
- baseNames.Add(trimmedName);
+ var file = remainingFiles[pos];
+ if (file.ExtraType == null)
+ {
+ continue;
+ }
+
+ var filename = file.FileNameWithoutExtension;
+ if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
+ || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
+ {
+ result.Add(file);
+ remainingFiles.RemoveAt(pos);
+ }
}
- return remainingFiles
- .Where(i => i.ExtraType != null)
- .Where(i => baseNames.Any(b =>
- i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
- .ToList();
+ return result;
}
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 619d1520e..c4ac5fdc6 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -1,45 +1,36 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolves <see cref="VideoFileInfo"/> from file path.
/// </summary>
- public class VideoResolver
+ public static class VideoResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="VideoResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
- /// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
- public VideoResolver(NamingOptions options)
- {
- _options = options;
- }
-
/// <summary>
/// Resolves the directory.
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>VideoFileInfo.</returns>
- public VideoFileInfo? ResolveDirectory(string? path)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
{
- return Resolve(path, true);
+ return Resolve(path, true, namingOptions);
}
/// <summary>
/// Resolves the file.
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>VideoFileInfo.</returns>
- public VideoFileInfo? ResolveFile(string? path)
+ public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
{
- return Resolve(path, false);
+ return Resolve(path, false, namingOptions);
}
/// <summary>
@@ -47,10 +38,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
/// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
- public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
+ public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
{
if (string.IsNullOrEmpty(path))
{
@@ -58,18 +50,18 @@ namespace Emby.Naming.Video
}
bool isStub = false;
- string? container = null;
+ ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
string? stubType = null;
if (!isDirectory)
{
- var extension = Path.GetExtension(path);
+ var extension = Path.GetExtension(path.AsSpan());
// Check supported extensions
- if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
- if (!StubResolver.TryResolveFile(path, _options, out stubType))
+ if (!StubResolver.TryResolveFile(path, namingOptions, out stubType))
{
return null;
}
@@ -80,25 +72,22 @@ namespace Emby.Naming.Video
container = extension.TrimStart('.');
}
- var flags = new FlagParser(_options).GetFlags(path);
- var format3DResult = new Format3DParser(_options).Parse(flags);
+ var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
+ var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
- var name = isDirectory
- ? Path.GetFileName(path)
- : Path.GetFileNameWithoutExtension(path);
+ var name = Path.GetFileNameWithoutExtension(path);
int? year = null;
if (parseName)
{
- var cleanDateTimeResult = CleanDateTime(name);
+ var cleanDateTimeResult = CleanDateTime(name, namingOptions);
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
- && TryCleanString(name, out ReadOnlySpan<char> newName))
+ && TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
{
name = newName.ToString();
}
@@ -106,7 +95,7 @@ namespace Emby.Naming.Video
return new VideoFileInfo(
path: path,
- container: container,
+ container: container.IsEmpty ? null : container.ToString(),
isStub: isStub,
name: name,
year: year,
@@ -122,43 +111,47 @@ namespace Emby.Naming.Video
/// Determines if path is video file based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>True if is video file.</returns>
- public bool IsVideoFile(string path)
+ public static bool IsVideoFile(string path, NamingOptions namingOptions)
{
- var extension = Path.GetExtension(path);
- return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ var extension = Path.GetExtension(path.AsSpan());
+ return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if path is video file stub based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>True if is video file stub.</returns>
- public bool IsStubFile(string path)
+ public static bool IsStubFile(string path, NamingOptions namingOptions)
{
- var extension = Path.GetExtension(path);
- return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ var extension = Path.GetExtension(path.AsSpan());
+ return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Tries to clean name of clutter.
/// </summary>
/// <param name="name">Raw name.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
- public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
+ public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
{
- return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
+ return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
/// <summary>
/// Tries to get name and year from raw name.
/// </summary>
/// <param name="name">Raw name.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
- public CleanDateTimeResult CleanDateTime(string name)
+ public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions)
{
- return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
+ return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes);
}
}
}
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 16ee918c4..5a2aea642 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -11,6 +11,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -25,14 +27,9 @@
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 62e33e6c4..2b6618159 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -24,18 +24,15 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 660bbb2de..6edfad575 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
- DataPath = Path.Combine(ProgramDataPath, "data");
+ _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <summary>
@@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
- public string DataPath
- {
- get => _dataPath;
- private set => _dataPath = Directory.CreateDirectory(value).FullName;
- }
+ public string DataPath => _dataPath;
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 4f72c8ce1..d38535634 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
+ /// <summary>
+ /// The _configuration sync lock.
+ /// </summary>
+ private readonly object _configurationSyncLock = new object();
+
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -32,11 +39,6 @@ namespace Emby.Server.Implementations.AppBase
private bool _configurationLoaded;
/// <summary>
- /// The _configuration sync lock.
- /// </summary>
- private readonly object _configurationSyncLock = new object();
-
- /// <summary>
/// The _configuration.
/// </summary>
private BaseApplicationConfiguration _configuration;
@@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
/// <inheritdoc />
public object GetConfiguration(string key)
{
- return _configurations.GetOrAdd(key, k =>
- {
- var file = GetConfigurationFile(key);
+ return _configurations.GetOrAdd(
+ key,
+ (k, configurationManager) =>
+ {
+ var file = configurationManager.GetConfigurationFile(k);
- var configurationInfo = _configurationStores
- .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
+ var configurationInfo = Array.Find(
+ configurationManager._configurationStores,
+ i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
- if (configurationInfo == null)
- {
- throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
- }
+ if (configurationInfo == null)
+ {
+ throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
+ }
- var configurationType = configurationInfo.ConfigurationType;
+ var configurationType = configurationInfo.ConfigurationType;
- lock (_configurationSyncLock)
- {
- return LoadConfiguration(file, configurationType);
- }
- });
+ lock (configurationManager._configurationSyncLock)
+ {
+ return configurationManager.LoadConfiguration(file, configurationType);
+ }
+ },
+ this);
}
private object LoadConfiguration(string path, Type configurationType)
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 77819c764..0308a68e4 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -1,9 +1,6 @@
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@@ -36,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception)
{
- configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
+ // Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
+ configuration = Activator.CreateInstance(type)!;
}
using var stream = new MemoryStream(buffer?.Length ?? 0);
@@ -53,7 +51,8 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory);
// Save it after load in case we got new items
- using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(newBytes, 0, newBytesLen);
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 1b9bb86bb..82995deb3 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,8 +12,6 @@ using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
@@ -43,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
+using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration;
@@ -50,7 +51,6 @@ using Jellyfin.Networking.Manager;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@@ -99,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
@@ -118,6 +119,7 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager;
+ private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
@@ -126,7 +128,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
/// <summary>
/// Gets a value indicating whether this instance can self restart.
@@ -211,7 +212,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
- protected IConfigurationManager ConfigurationManager { get; set; }
+ public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets the service provider.
@@ -229,10 +230,9 @@ namespace Emby.Server.Implementations
public int HttpsPort { get; private set; }
/// <summary>
- /// Gets the server configuration manager.
+ /// Gets the value of the PublishedServerUrl setting.
/// </summary>
- /// <value>The server configuration manager.</value>
- public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
+ public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -240,51 +240,37 @@ namespace Emby.Server.Implementations
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
+ /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
+ IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{
- _xmlSerializer = new MyXmlSerializer();
-
- ServiceCollection = serviceCollection;
-
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
+ _startupOptions = options;
+ _startupConfig = startupConfig;
_fileSystemManager = fileSystem;
-
- ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
- // Have to migrate settings here as migration subsystem not yet initialised.
- MigrateNetworkConfiguration();
-
- // Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
- ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
- NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
+ ServiceCollection = serviceCollection;
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
-
- _startupOptions = options;
-
- // Initialize runtime stat collection
- if (ServerConfigurationManager.Configuration.EnableMetrics)
- {
- DotNetRuntimeStatsBuilder.Default().StartCollecting();
- }
-
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+ _xmlSerializer = new MyXmlSerializer();
+ ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
- ServerConfigurationManager.Configuration,
+ ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
}
@@ -299,9 +285,9 @@ namespace Emby.Server.Implementations
if (!File.Exists(path))
{
var networkSettings = new NetworkConfiguration();
- ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
+ ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path);
- Logger?.LogDebug("Successfully migrated network settings.");
+ Logger.LogDebug("Successfully migrated network settings.");
}
}
@@ -351,10 +337,7 @@ namespace Emby.Server.Implementations
{
get
{
- if (_deviceId == null)
- {
- _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
- }
+ _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
@@ -374,7 +357,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Creates an instance of type and resolves all constructor dependencies.
/// </summary>
- /// /// <typeparam name="T">The type.</typeparam>
+ /// <typeparam name="T">The type.</typeparam>
/// <returns>T.</returns>
public T CreateInstance<T>()
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
@@ -386,10 +369,7 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type)
{
- if (_creatingInstances == null)
- {
- _creatingInstances = new List<Type>();
- }
+ _creatingInstances ??= new List<Type>();
if (_creatingInstances.IndexOf(type) != -1)
{
@@ -460,7 +440,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc />
- public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
+ public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
{
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>()
@@ -484,8 +464,9 @@ namespace Emby.Server.Implementations
/// Runs the startup tasks.
/// </summary>
/// <returns><see cref="Task" />.</returns>
- public async Task RunStartupTasksAsync()
+ public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{
+ cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@@ -499,14 +480,21 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>();
+ cancellationToken.ThrowIfCancellationRequested();
+
var stopWatch = new Stopwatch();
stopWatch.Start();
+
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
stopWatch.Restart();
+
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
stopWatch.Stop();
@@ -530,7 +518,21 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public void Init()
{
- var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
+ DiscoverTypes();
+
+ ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
+
+ // Have to migrate settings here as migration subsystem not yet initialised.
+ MigrateNetworkConfiguration();
+ NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
+
+ // Initialize runtime stat collection
+ if (ConfigurationManager.Configuration.EnableMetrics)
+ {
+ DotNetRuntimeStatsBuilder.Default().StartCollecting();
+ }
+
+ var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
HttpPort = networkConfiguration.HttpServerPortNumber;
HttpsPort = networkConfiguration.HttpsPortNumber;
@@ -548,8 +550,6 @@ namespace Emby.Server.Implementations
};
Certificate = GetCertificate(CertificateInfo);
- DiscoverTypes();
-
RegisterServices();
_pluginManager.RegisterServices(ServiceCollection);
@@ -564,7 +564,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddMemoryCache();
- ServiceCollection.AddSingleton(ConfigurationManager);
+ ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
+ ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
@@ -591,8 +592,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
- ServiceCollection.AddSingleton(ServerConfigurationManager);
-
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
@@ -604,12 +603,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
- // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
-
- // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+ ServiceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -674,14 +669,14 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
- ServiceCollection.AddSingleton<EncodingHelper>();
-
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>();
+
+ ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
}
/// <summary>
@@ -779,7 +774,7 @@ namespace Emby.Server.Implementations
{
// For now there's no real way to inject these properly
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
- BaseItem.ConfigurationManager = ServerConfigurationManager;
+ BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
@@ -801,13 +796,12 @@ namespace Emby.Server.Implementations
/// </summary>
private void FindParts()
{
- if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
+ if (!ConfigurationManager.Configuration.IsPortAuthorized)
{
- ServerConfigurationManager.Configuration.IsPortAuthorized = true;
+ ConfigurationManager.Configuration.IsPortAuthorized = true;
ConfigurationManager.SaveConfiguration();
}
- ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
@@ -911,7 +905,7 @@ namespace Emby.Server.Implementations
protected void OnConfigurationUpdated(object sender, EventArgs e)
{
var requiresRestart = false;
- var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
+ var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
// Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0)
@@ -920,10 +914,10 @@ namespace Emby.Server.Implementations
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort)
{
- if (ServerConfigurationManager.Configuration.IsPortAuthorized)
+ if (ConfigurationManager.Configuration.IsPortAuthorized)
{
- ServerConfigurationManager.Configuration.IsPortAuthorized = false;
- ServerConfigurationManager.SaveConfiguration();
+ ConfigurationManager.Configuration.IsPortAuthorized = false;
+ ConfigurationManager.SaveConfiguration();
requiresRestart = true;
}
@@ -1139,16 +1133,16 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
- public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
+ public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{
// Published server ends with a /
- if (_startupOptions.PublishedServerUrl != null)
+ if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
- return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+ return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(ipAddress, out port);
@@ -1165,10 +1159,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
// Published server ends with a /
- if (_startupOptions.PublishedServerUrl != null)
+ if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
- return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+ return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(request, out port);
@@ -1185,10 +1179,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(string hostname, int? port = null)
{
// Published server ends with a /
- if (_startupOptions.PublishedServerUrl != null)
+ if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
- return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+ return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(hostname, out port);
@@ -1223,14 +1217,14 @@ namespace Emby.Server.Implementations
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
Host = host,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
- Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
+ Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/');
}
public string FriendlyName =>
- string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
+ string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
- : ServerConfigurationManager.Configuration.ServerName;
+ : ConfigurationManager.Configuration.ServerName;
/// <summary>
/// Shuts down.
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 8c5fa09f6..448f12403 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1,9 +1,10 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary>
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
index c69a07e83..ca8409402 100644
--- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
+++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
@@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
return null;
})
.Where(i => i != null)
- .GroupBy(x => x.Id)
+ .GroupBy(x => x!.Id) // We removed the null values
.Select(x => x.First())
- .ToList();
+ .ToList()!; // Again... the list doesn't contain any null values
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 3011a37e3..82d80fc83 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -1,6 +1,7 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
@@ -8,11 +9,9 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -107,7 +106,7 @@ namespace Emby.Server.Implementations.Collections
var name = _localizationManager.GetLocalizedString("Collections");
- await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
+ await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
return FindFolders(path).First();
}
@@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections
private IEnumerable<BoxSet> GetCollections(User user)
{
- var folder = GetCollectionsFolder(false).Result;
+ var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
return folder == null
? Enumerable.Empty<BoxSet>()
@@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
parentFolder.AddChild(collection, CancellationToken.None);
- if (options.ItemIdList.Length > 0)
+ if (options.ItemIdList.Count > 0)
{
await AddToCollectionAsync(
collection.Id,
@@ -251,11 +250,7 @@ namespace Emby.Server.Implementations.Collections
if (fireEvent)
{
- ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
- {
- Collection = collection,
- ItemsChanged = itemList
- });
+ ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
}
}
}
@@ -307,11 +302,7 @@ namespace Emby.Server.Implementations.Collections
},
RefreshPriority.High);
- ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
- {
- Collection = collection,
- ItemsChanged = itemList
- });
+ ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
}
/// <inheritdoc />
@@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections
{
var results = new Dictionary<Guid, BaseItem>();
- var allBoxsets = GetCollections(user).ToList();
+ var allBoxSets = GetCollections(user).ToList();
foreach (var item in items)
{
- if (!(item is ISupportsBoxSetGrouping))
+ if (item is not ISupportsBoxSetGrouping)
{
results[item.Id] = item;
}
@@ -331,20 +322,44 @@ namespace Emby.Server.Implementations.Collections
{
var itemId = item.Id;
- var currentBoxSets = allBoxsets
- .Where(i => i.ContainsLinkedChildByItemId(itemId))
- .ToList();
+ var itemIsInBoxSet = false;
+ foreach (var boxSet in allBoxSets)
+ {
+ if (!boxSet.ContainsLinkedChildByItemId(itemId))
+ {
+ continue;
+ }
+
+ itemIsInBoxSet = true;
- if (currentBoxSets.Count > 0)
+ results.TryAdd(boxSet.Id, boxSet);
+ }
+
+ // skip any item that is in a box set
+ if (itemIsInBoxSet)
{
- foreach (var boxset in currentBoxSets)
+ continue;
+ }
+
+ var alreadyInResults = false;
+ // this is kind of a performance hack because only Video has alternate versions that should be in a box set?
+ if (item is Video video)
+ {
+ foreach (var childId in video.GetLocalAlternateVersionIds())
{
- results[boxset.Id] = boxset;
+ if (!results.ContainsKey(childId))
+ {
+ continue;
+ }
+
+ alreadyInResults = true;
+ break;
}
}
- else
+
+ if (!alreadyInResults)
{
- results[item.Id] = item;
+ results[itemId] = item;
}
}
}
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index 7a8ed8c29..ff5602f24 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Globalization;
using System.IO;
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index cd9dbb1bd..01dc728c1 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using Emby.Server.Implementations.HttpServer;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 12a9e44e7..4a9b28085 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 8c756a7f4..6f23a0888 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
{
- if (row[1].SQLiteType != SQLiteType.Null)
+ if (row.TryGetString(1, out var columnName))
{
- var name = row[1].ToString();
-
- columnNames.Add(name);
+ columnNames.Add(columnName);
}
}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
index 5c094ddd2..afc8966f9 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data
{
public class ManagedConnection : IDisposable
{
- private SQLiteDatabaseConnection _db;
+ private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
private bool _disposed = false;
@@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data
return _db.RunInTransaction(action, mode);
}
- public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql)
+ public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
{
return _db.Query(sql);
}
- public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values)
+ public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
{
return _db.Query(sql, values);
}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 1af301ceb..3289e7609 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -1,7 +1,9 @@
+#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using SQLitePCL.pretty;
@@ -59,11 +61,11 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(conn =>
{
- conn.ExecuteAll(string.Join(";", queries));
+ conn.ExecuteAll(string.Join(';', queries));
});
}
- public static Guid ReadGuidFromBlob(this IResultSetValue result)
+ public static Guid ReadGuidFromBlob(this ResultSetValue result)
{
return new Guid(result.ToBlob());
}
@@ -84,7 +86,7 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
- public static DateTime ReadDateTime(this IResultSetValue result)
+ public static DateTime ReadDateTime(this ResultSetValue result)
{
var dateText = result.ToString();
@@ -95,58 +97,147 @@ namespace Emby.Server.Implementations.Data
DateTimeStyles.None).ToUniversalTime();
}
- public static DateTime? TryReadDateTime(this IResultSetValue result)
+ public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
{
- var dateText = result.ToString();
+ var item = reader[index];
+ if (item.IsDbNull())
+ {
+ result = default;
+ return false;
+ }
+
+ var dateText = item.ToString();
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
{
- return dateTimeResult.ToUniversalTime();
+ result = dateTimeResult.ToUniversalTime();
+ return true;
+ }
+
+ result = default;
+ return false;
+ }
+
+ public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
+ {
+ var item = reader[index];
+ if (item.IsDbNull())
+ {
+ result = default;
+ return false;
}
- return null;
+ result = item.ReadGuidFromBlob();
+ return true;
}
- public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index)
+ public static bool IsDbNull(this ResultSetValue result)
{
- return result[index].SQLiteType == SQLiteType.Null;
+ return result.SQLiteType == SQLiteType.Null;
}
- public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
+ public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToString();
}
- public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
+ public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
+ {
+ result = null;
+ var item = reader[index];
+ if (item.IsDbNull())
+ {
+ return false;
+ }
+
+ result = item.ToString();
+ return true;
+ }
+
+ public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToBool();
}
- public static int GetInt32(this IReadOnlyList<IResultSetValue> result, int index)
+ public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
+ {
+ var item = reader[index];
+ if (item.IsDbNull())
+ {
+ result = default;
+ return false;
+ }
+
+ result = item.ToBool();
+ return true;
+ }
+
+ public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
{
- return result[index].ToInt();
+ var item = reader[index];
+ if (item.IsDbNull())
+ {
+ result = default;
+ return false;
+ }
+
+ result = item.ToInt();
+ return true;
}
- public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
+ public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToInt64();
}
- public static float GetFloat(this IReadOnlyList<IResultSetValue> result, int index)
+ public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
+ {
+ var item = reader[index];
+ if (item.IsDbNull())
+ {
+ result = default;
+ return false;
+ }
+
+ result = item.ToInt64();
+ return true;
+ }
+
+ public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
+ {
+ var item = reader[index];
+ if (item.IsDbNull())
+ {
+ result = default;
+ return false;
+ }
+
+ result = item.ToFloat();
+ return true;
+ }
+
+ public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
{
- return result[index].ToFloat();
+ var item = reader[index];
+ if (item.IsDbNull())
+ {
+ result = default;
+ return false;
+ }
+
+ result = item.ToDouble();
+ return true;
}
- public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)
+ public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ReadGuidFromBlob();
}
+ [Conditional("DEBUG")]
private static void CheckName(string name)
{
-#if DEBUG
throw new ArgumentException("Invalid param name: " + name, nameof(name));
-#endif
}
public static void TryBind(this IStatement statement, string name, double value)
@@ -350,7 +441,7 @@ namespace Emby.Server.Implementations.Data
}
}
- public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
+ public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
{
while (statement.MoveNext())
{
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 6e1f2feae..9b147b5d7 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1,6 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
+using System.Buffers.Text;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -40,6 +43,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
{
+ private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
private readonly IServerConfigurationManager _config;
@@ -88,7 +92,7 @@ namespace Emby.Server.Implementations.Data
_imageProcessor = imageProcessor;
_typeMapper = new TypeMapper();
- _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions = JsonDefaults.Options;
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
}
@@ -502,7 +506,7 @@ namespace Emby.Server.Implementations.Data
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
- saveImagesStatement.TryBind("@Images", SerializeImages(item));
+ saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
saveImagesStatement.MoveNext();
}
@@ -687,7 +691,7 @@ namespace Emby.Server.Implementations.Data
if (item.Genres.Length > 0)
{
- saveItemStatement.TryBind("@Genres", string.Join("|", item.Genres));
+ saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres));
}
else
{
@@ -749,7 +753,7 @@ namespace Emby.Server.Implementations.Data
if (item.LockedFields.Length > 0)
{
- saveItemStatement.TryBind("@LockedFields", string.Join("|", item.LockedFields));
+ saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields));
}
else
{
@@ -758,7 +762,7 @@ namespace Emby.Server.Implementations.Data
if (item.Studios.Length > 0)
{
- saveItemStatement.TryBind("@Studios", string.Join("|", item.Studios));
+ saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios));
}
else
{
@@ -785,7 +789,7 @@ namespace Emby.Server.Implementations.Data
if (item.Tags.Length > 0)
{
- saveItemStatement.TryBind("@Tags", string.Join("|", item.Tags));
+ saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags));
}
else
{
@@ -807,7 +811,7 @@ namespace Emby.Server.Implementations.Data
if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
{
- saveItemStatement.TryBind("@TrailerTypes", string.Join("|", trailer.TrailerTypes));
+ saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes));
}
else
{
@@ -897,12 +901,12 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId);
saveItemStatement.TryBind("@Tagline", item.Tagline);
- saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item));
- saveItemStatement.TryBind("@Images", SerializeImages(item));
+ saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds));
+ saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
if (item.ProductionLocations.Length > 0)
{
- saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations));
+ saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
}
else
{
@@ -911,7 +915,7 @@ namespace Emby.Server.Implementations.Data
if (item.ExtraIds.Length > 0)
{
- saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds));
+ saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds));
}
else
{
@@ -931,7 +935,7 @@ namespace Emby.Server.Implementations.Data
string artists = null;
if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
{
- artists = string.Join("|", hasArtists.Artists);
+ artists = string.Join('|', hasArtists.Artists);
}
saveItemStatement.TryBind("@Artists", artists);
@@ -940,7 +944,7 @@ namespace Emby.Server.Implementations.Data
if (item is IHasAlbumArtist hasAlbumArtists
&& hasAlbumArtists.AlbumArtists.Count > 0)
{
- albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists);
+ albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists);
}
saveItemStatement.TryBind("@AlbumArtists", albumArtists);
@@ -968,10 +972,10 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.MoveNext();
}
- private static string SerializeProviderIds(BaseItem item)
+ internal static string SerializeProviderIds(Dictionary<string, string> providerIds)
{
StringBuilder str = new StringBuilder();
- foreach (var i in item.ProviderIds)
+ foreach (var i in providerIds)
{
// Ideally we shouldn't need this IsNullOrWhiteSpace check,
// but we're seeing some cases of bad data slip through
@@ -995,35 +999,25 @@ namespace Emby.Server.Implementations.Data
return str.ToString();
}
- private static void DeserializeProviderIds(string value, BaseItem item)
+ internal static void DeserializeProviderIds(string value, IHasProviderIds item)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
- if (item.ProviderIds.Count > 0)
- {
- return;
- }
-
- var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
-
- foreach (var part in parts)
+ foreach (var part in value.SpanSplit('|'))
{
- var idParts = part.Split('=');
-
- if (idParts.Length == 2)
+ var providerDelimiterIndex = part.IndexOf('=');
+ if (providerDelimiterIndex != -1 && providerDelimiterIndex == part.LastIndexOf('='))
{
- item.SetProviderId(idParts[0], idParts[1]);
+ item.SetProviderId(part.Slice(0, providerDelimiterIndex).ToString(), part.Slice(providerDelimiterIndex + 1).ToString());
}
}
}
- private string SerializeImages(BaseItem item)
+ internal string SerializeImages(ItemImageInfo[] images)
{
- var images = item.ImageInfos;
-
if (images.Length == 0)
{
return null;
@@ -1045,39 +1039,48 @@ namespace Emby.Server.Implementations.Data
return str.ToString();
}
- private void DeserializeImages(string value, BaseItem item)
+ internal ItemImageInfo[] DeserializeImages(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
- return;
+ return Array.Empty<ItemImageInfo>();
}
- if (item.ImageInfos.Length > 0)
- {
- return;
- }
+ // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
+ var valueSpan = value.AsSpan();
+ var count = valueSpan.Count('|') + 1;
- var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
- var list = new List<ItemImageInfo>();
- foreach (var part in parts)
+ var position = 0;
+ var result = new ItemImageInfo[count];
+ foreach (var part in valueSpan.Split('|'))
{
var image = ItemImageInfoFromValueString(part);
if (image != null)
{
- list.Add(image);
+ result[position++] = image;
}
}
- item.ImageInfos = list.ToArray();
+ if (position == count)
+ {
+ return result;
+ }
+
+ if (position == 0)
+ {
+ return Array.Empty<ItemImageInfo>();
+ }
+
+ // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
+ return result[..position];
}
- public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
+ private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
{
const char Delimiter = '*';
var path = image.Path ?? string.Empty;
- var hash = image.BlurHash ?? string.Empty;
bldr.Append(GetPathToSave(path))
.Append(Delimiter)
@@ -1087,48 +1090,105 @@ namespace Emby.Server.Implementations.Data
.Append(Delimiter)
.Append(image.Width)
.Append(Delimiter)
- .Append(image.Height)
- .Append(Delimiter)
- // Replace delimiters with other characters.
- // This can be removed when we migrate to a proper DB.
- .Append(hash.Replace('*', '/').Replace('|', '\\'));
+ .Append(image.Height);
+
+ var hash = image.BlurHash;
+ if (!string.IsNullOrEmpty(hash))
+ {
+ bldr.Append(Delimiter)
+ // Replace delimiters with other characters.
+ // This can be removed when we migrate to a proper DB.
+ .Append(hash.Replace('*', '/').Replace('|', '\\'));
+ }
}
- public ItemImageInfo ItemImageInfoFromValueString(string value)
+ internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value)
{
- var parts = value.Split('*', StringSplitOptions.None);
+ var nextSegment = value.IndexOf('*');
+ if (nextSegment == -1)
+ {
+ return null;
+ }
- if (parts.Length < 3)
+ ReadOnlySpan<char> path = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf('*');
+ if (nextSegment == -1)
{
return null;
}
- var image = new ItemImageInfo();
+ ReadOnlySpan<char> dateModified = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf('*');
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
- image.Path = RestorePath(parts[0]);
+ ReadOnlySpan<char> imageType = value[..nextSegment];
- if (long.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks))
+ var image = new ItemImageInfo
+ {
+ Path = RestorePath(path.ToString())
+ };
+
+ if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks))
{
image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
}
- if (Enum.TryParse(parts[2], true, out ImageType type))
+ if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
{
image.Type = type;
}
- if (parts.Length >= 5)
+ // Optional parameters: width*height*blurhash
+ if (nextSegment + 1 < value.Length - 1)
{
- if (int.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
- && int.TryParse(parts[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf('*');
+ if (nextSegment == -1 || nextSegment == value.Length)
+ {
+ return image;
+ }
+
+ ReadOnlySpan<char> widthSpan = value[..nextSegment];
+
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf('*');
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
+
+ ReadOnlySpan<char> heightSpan = value[..nextSegment];
+
+ if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
+ && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
{
image.Width = width;
image.Height = height;
}
- if (parts.Length >= 6)
+ if (nextSegment < value.Length - 1)
{
- image.BlurHash = parts[5].Replace('/', '*').Replace('\\', '|');
+ value = value[(nextSegment + 1)..];
+ var length = value.Length;
+
+ Span<char> blurHashSpan = stackalloc char[length];
+ for (int i = 0; i < length; i++)
+ {
+ var c = value[i];
+ blurHashSpan[i] = c switch
+ {
+ '/' => '*',
+ '\\' => '|',
+ _ => c
+ };
+ }
+
+ image.BlurHash = new string(blurHashSpan);
}
}
@@ -1241,12 +1301,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- private BaseItem GetItem(IReadOnlyList<IResultSetValue> reader, InternalItemsQuery query)
+ private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query)
{
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query));
}
- private BaseItem GetItem(IReadOnlyList<IResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
+ private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
{
var typeString = reader.GetString(0);
@@ -1291,27 +1351,30 @@ namespace Emby.Server.Implementations.Data
if (queryHasStartDate)
{
- if (!reader.IsDBNull(index))
+ if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate))
{
- if (item is IHasStartDate hasStartDate)
- {
- hasStartDate.StartDate = reader[index].ReadDateTime();
- }
+ hasStartDate.StartDate = startDate;
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var endDate))
{
- item.EndDate = reader[index].TryReadDateTime();
+ item.EndDate = endDate;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ var channelId = reader[index];
+ if (!channelId.IsDbNull())
{
- item.ChannelId = new Guid(reader.GetString(index));
+ if (!Utf8Parser.TryParse(channelId.ToBlob(), out Guid value, out _, standardFormat: 'N'))
+ {
+ var str = reader.GetString(index);
+ Logger.LogWarning("{ChannelId} isn't in the expected format", str);
+ value = new Guid(str);
+ }
+
+ item.ChannelId = value;
}
index++;
@@ -1320,33 +1383,25 @@ namespace Emby.Server.Implementations.Data
{
if (item is IHasProgramAttributes hasProgramAttributes)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isMovie))
{
- hasProgramAttributes.IsMovie = reader.GetBoolean(index);
+ hasProgramAttributes.IsMovie = isMovie;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isSeries))
{
- hasProgramAttributes.IsSeries = reader.GetBoolean(index);
+ hasProgramAttributes.IsSeries = isSeries;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var episodeTitle))
{
- hasProgramAttributes.EpisodeTitle = reader.GetString(index);
+ hasProgramAttributes.EpisodeTitle = episodeTitle;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isRepeat))
{
- hasProgramAttributes.IsRepeat = reader.GetBoolean(index);
+ hasProgramAttributes.IsRepeat = isRepeat;
}
-
- index++;
}
else
{
@@ -1354,242 +1409,190 @@ namespace Emby.Server.Implementations.Data
}
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetSingle(index++, out var communityRating))
{
- item.CommunityRating = reader.GetFloat(index);
+ item.CommunityRating = communityRating;
}
- index++;
-
if (HasField(query, ItemFields.CustomRating))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var customRating))
{
- item.CustomRating = reader.GetString(index);
+ item.CustomRating = customRating;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var indexNumber))
{
- item.IndexNumber = reader.GetInt32(index);
+ item.IndexNumber = indexNumber;
}
- index++;
-
if (HasField(query, ItemFields.Settings))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isLocked))
{
- item.IsLocked = reader.GetBoolean(index);
+ item.IsLocked = isLocked;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var preferredMetadataLanguage))
{
- item.PreferredMetadataLanguage = reader.GetString(index);
+ item.PreferredMetadataLanguage = preferredMetadataLanguage;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
{
- item.PreferredMetadataCountryCode = reader.GetString(index);
+ item.PreferredMetadataCountryCode = preferredMetadataCountryCode;
}
-
- index++;
}
if (HasField(query, ItemFields.Width))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var width))
{
- item.Width = reader.GetInt32(index);
+ item.Width = width;
}
-
- index++;
}
if (HasField(query, ItemFields.Height))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var height))
{
- item.Height = reader.GetInt32(index);
+ item.Height = height;
}
-
- index++;
}
if (HasField(query, ItemFields.DateLastRefreshed))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
{
- item.DateLastRefreshed = reader[index].ReadDateTime();
+ item.DateLastRefreshed = dateLastRefreshed;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var name))
{
- item.Name = reader.GetString(index);
+ item.Name = name;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var restorePath))
{
- item.Path = RestorePath(reader.GetString(index));
+ item.Path = RestorePath(restorePath);
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var premiereDate))
{
- item.PremiereDate = reader[index].TryReadDateTime();
+ item.PremiereDate = premiereDate;
}
- index++;
-
if (HasField(query, ItemFields.Overview))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var overview))
{
- item.Overview = reader.GetString(index);
+ item.Overview = overview;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var parentIndexNumber))
{
- item.ParentIndexNumber = reader.GetInt32(index);
+ item.ParentIndexNumber = parentIndexNumber;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var productionYear))
{
- item.ProductionYear = reader.GetInt32(index);
+ item.ProductionYear = productionYear;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var officialRating))
{
- item.OfficialRating = reader.GetString(index);
+ item.OfficialRating = officialRating;
}
- index++;
-
if (HasField(query, ItemFields.SortName))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var forcedSortName))
{
- item.ForcedSortName = reader.GetString(index);
+ item.ForcedSortName = forcedSortName;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt64(index++, out var runTimeTicks))
{
- item.RunTimeTicks = reader.GetInt64(index);
+ item.RunTimeTicks = runTimeTicks;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt64(index++, out var size))
{
- item.Size = reader.GetInt64(index);
+ item.Size = size;
}
- index++;
-
if (HasField(query, ItemFields.DateCreated))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var dateCreated))
{
- item.DateCreated = reader[index].ReadDateTime();
+ item.DateCreated = dateCreated;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var dateModified))
{
- item.DateModified = reader[index].ReadDateTime();
+ item.DateModified = dateModified;
}
- index++;
-
- item.Id = reader.GetGuid(index);
- index++;
+ item.Id = reader.GetGuid(index++);
if (HasField(query, ItemFields.Genres))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var genres))
{
- item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetGuid(index++, out var parentId))
{
- item.ParentId = reader.GetGuid(index);
+ item.ParentId = parentId;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var audioString))
{
- if (Enum.TryParse(reader.GetString(index), true, out ProgramAudio audio))
+ // TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916
+ if (Enum.TryParse(audioString, true, out ProgramAudio audio))
{
item.Audio = audio;
}
}
- index++;
-
// TODO: Even if not needed by apps, the server needs it internally
// But get this excluded from contexts where it is not needed
if (hasServiceName)
{
if (item is LiveTvChannel liveTvChannel)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var serviceName))
{
- liveTvChannel.ServiceName = reader.GetString(index);
+ liveTvChannel.ServiceName = serviceName;
}
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isInMixedFolder))
{
- item.IsInMixedFolder = reader.GetBoolean(index);
+ item.IsInMixedFolder = isInMixedFolder;
}
- index++;
-
if (HasField(query, ItemFields.DateLastSaved))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryReadDateTime(index++, out var dateLastSaved))
{
- item.DateLastSaved = reader[index].ReadDateTime();
+ item.DateLastSaved = dateLastSaved;
}
-
- index++;
}
if (HasField(query, ItemFields.Settings))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var lockedFields))
{
IEnumerable<MetadataField> GetLockedFields(string s)
{
@@ -1602,37 +1605,31 @@ namespace Emby.Server.Implementations.Data
}
}
- item.LockedFields = GetLockedFields(reader.GetString(index)).ToArray();
+ item.LockedFields = GetLockedFields(lockedFields).ToArray();
}
-
- index++;
}
if (HasField(query, ItemFields.Studios))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var studios))
{
- item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
-
- index++;
}
if (HasField(query, ItemFields.Tags))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var tags))
{
- item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
-
- index++;
}
if (hasTrailerTypes)
{
if (item is Trailer trailer)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var trailerTypes))
{
IEnumerable<TrailerType> GetTrailerTypes(string s)
{
@@ -1645,7 +1642,7 @@ namespace Emby.Server.Implementations.Data
}
}
- trailer.TrailerTypes = GetTrailerTypes(reader.GetString(index)).ToArray();
+ trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray();
}
}
@@ -1654,19 +1651,17 @@ namespace Emby.Server.Implementations.Data
if (HasField(query, ItemFields.OriginalTitle))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var originalTitle))
{
- item.OriginalTitle = reader.GetString(index);
+ item.OriginalTitle = originalTitle;
}
-
- index++;
}
if (item is Video video)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var primaryVersionId))
{
- video.PrimaryVersionId = reader.GetString(index);
+ video.PrimaryVersionId = primaryVersionId;
}
}
@@ -1674,40 +1669,34 @@ namespace Emby.Server.Implementations.Data
if (HasField(query, ItemFields.DateLastMediaAdded))
{
- if (item is Folder folder && !reader.IsDBNull(index))
+ if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded))
{
- folder.DateLastMediaAdded = reader[index].TryReadDateTime();
+ folder.DateLastMediaAdded = dateLastMediaAdded;
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var album))
{
- item.Album = reader.GetString(index);
+ item.Album = album;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetSingle(index++, out var criticRating))
{
- item.CriticRating = reader.GetFloat(index);
+ item.CriticRating = criticRating;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetBoolean(index++, out var isVirtualItem))
{
- item.IsVirtualItem = reader.GetBoolean(index);
+ item.IsVirtualItem = isVirtualItem;
}
- index++;
-
if (item is IHasSeries hasSeriesName)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var seriesName))
{
- hasSeriesName.SeriesName = reader.GetString(index);
+ hasSeriesName.SeriesName = seriesName;
}
}
@@ -1717,15 +1706,15 @@ namespace Emby.Server.Implementations.Data
{
if (item is Episode episode)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var seasonName))
{
- episode.SeasonName = reader.GetString(index);
+ episode.SeasonName = seasonName;
}
index++;
- if (!reader.IsDBNull(index))
+ if (reader.TryGetGuid(index, out var seasonId))
{
- episode.SeasonId = reader.GetGuid(index);
+ episode.SeasonId = seasonId;
}
}
else
@@ -1741,9 +1730,9 @@ namespace Emby.Server.Implementations.Data
{
if (hasSeries != null)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetGuid(index, out var seriesId))
{
- hasSeries.SeriesId = reader.GetGuid(index);
+ hasSeries.SeriesId = seriesId;
}
}
@@ -1752,56 +1741,48 @@ namespace Emby.Server.Implementations.Data
if (HasField(query, ItemFields.PresentationUniqueKey))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var presentationUniqueKey))
{
- item.PresentationUniqueKey = reader.GetString(index);
+ item.PresentationUniqueKey = presentationUniqueKey;
}
-
- index++;
}
if (HasField(query, ItemFields.InheritedParentalRatingValue))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var parentalRating))
{
- item.InheritedParentalRatingValue = reader.GetInt32(index);
+ item.InheritedParentalRatingValue = parentalRating;
}
-
- index++;
}
if (HasField(query, ItemFields.ExternalSeriesId))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var externalSeriesId))
{
- item.ExternalSeriesId = reader.GetString(index);
+ item.ExternalSeriesId = externalSeriesId;
}
-
- index++;
}
if (HasField(query, ItemFields.Taglines))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var tagLine))
{
- item.Tagline = reader.GetString(index);
+ item.Tagline = tagLine;
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds))
{
- DeserializeProviderIds(reader.GetString(index), item);
+ DeserializeProviderIds(providerIds, item);
}
index++;
if (query.DtoOptions.EnableImages)
{
- if (!reader.IsDBNull(index))
+ if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos))
{
- DeserializeImages(reader.GetString(index), item);
+ item.ImageInfos = DeserializeImages(imageInfos);
}
index++;
@@ -1809,72 +1790,62 @@ namespace Emby.Server.Implementations.Data
if (HasField(query, ItemFields.ProductionLocations))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var productionLocations))
{
- item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray();
+ item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
-
- index++;
}
if (HasField(query, ItemFields.ExtraIds))
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var extraIds))
{
- item.ExtraIds = SplitToGuids(reader.GetString(index));
+ item.ExtraIds = SplitToGuids(extraIds);
}
-
- index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetInt32(index++, out var totalBitrate))
{
- item.TotalBitrate = reader.GetInt32(index);
+ item.TotalBitrate = totalBitrate;
}
- index++;
-
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var extraTypeString))
{
- if (Enum.TryParse(reader.GetString(index), true, out ExtraType extraType))
+ if (Enum.TryParse(extraTypeString, true, out ExtraType extraType))
{
item.ExtraType = extraType;
}
}
- index++;
-
if (hasArtistFields)
{
- if (item is IHasArtist hasArtists && !reader.IsDBNull(index))
+ if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists))
{
- hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
- if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index))
+ if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists))
{
- hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
+ hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index++, out var externalId))
{
- item.ExternalId = reader.GetString(index);
+ item.ExternalId = externalId;
}
- index++;
-
if (HasField(query, ItemFields.SeriesPresentationUniqueKey))
{
if (hasSeries != null)
{
- if (!reader.IsDBNull(index))
+ if (reader.TryGetString(index, out var seriesPresentationUniqueKey))
{
- hasSeries.SeriesPresentationUniqueKey = reader.GetString(index);
+ hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
}
}
@@ -1883,21 +1854,19 @@ namespace Emby.Server.Implementations.Data
if (enableProgramAttributes)
{
- if (item is LiveTvProgram program && !reader.IsDBNull(index))
+ if (item is LiveTvProgram program && reader.TryGetString(index, out var showId))
{
- program.ShowId = reader.GetString(index);
+ program.ShowId = showId;
}
index++;
}
- if (!reader.IsDBNull(index))
+ if (reader.TryGetGuid(index, out var ownerId))
{
- item.OwnerId = reader.GetGuid(index);
+ item.OwnerId = ownerId;
}
- index++;
-
return item;
}
@@ -1977,21 +1946,21 @@ namespace Emby.Server.Implementations.Data
/// <param name="reader">The reader.</param>
/// <param name="item">The item.</param>
/// <returns>ChapterInfo.</returns>
- private ChapterInfo GetChapter(IReadOnlyList<IResultSetValue> reader, BaseItem item)
+ private ChapterInfo GetChapter(IReadOnlyList<ResultSetValue> reader, BaseItem item)
{
var chapter = new ChapterInfo
{
StartPositionTicks = reader.GetInt64(0)
};
- if (!reader.IsDBNull(1))
+ if (reader.TryGetString(1, out var chapterName))
{
- chapter.Name = reader.GetString(1);
+ chapter.Name = chapterName;
}
- if (!reader.IsDBNull(2))
+ if (reader.TryGetString(2, out var imagePath))
{
- chapter.ImagePath = reader.GetString(2);
+ chapter.ImagePath = imagePath;
if (!string.IsNullOrEmpty(chapter.ImagePath))
{
@@ -2006,9 +1975,9 @@ namespace Emby.Server.Implementations.Data
}
}
- if (!reader.IsDBNull(3))
+ if (reader.TryReadDateTime(3, out var imageDateModified))
{
- chapter.ImageDateModified = reader[3].ReadDateTime();
+ chapter.ImageDateModified = imageDateModified;
}
return chapter;
@@ -2116,30 +2085,7 @@ namespace Emby.Server.Implementations.Data
|| query.IsLiked.HasValue;
}
- private readonly ItemFields[] _allFields = Enum.GetNames(typeof(ItemFields))
- .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
- .ToArray();
-
- private string[] GetColumnNamesFromField(ItemFields field)
- {
- switch (field)
- {
- case ItemFields.Settings:
- return new[] { "IsLocked", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "LockedFields" };
- case ItemFields.ServiceName:
- return new[] { "ExternalServiceId" };
- case ItemFields.SortName:
- return new[] { "ForcedSortName" };
- case ItemFields.Taglines:
- return new[] { "Tagline" };
- case ItemFields.Tags:
- return new[] { "Tags" };
- case ItemFields.IsHD:
- return Array.Empty<string>();
- default:
- return new[] { field.ToString() };
- }
- }
+ private readonly ItemFields[] _allFields = Enum.GetValues<ItemFields>();
private bool HasField(InternalItemsQuery query, ItemFields name)
{
@@ -2321,77 +2267,98 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
}
- private List<string> GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable<string> startColumns)
+ private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
{
- var list = startColumns.ToList();
-
foreach (var field in _allFields)
{
if (!HasField(query, field))
{
- foreach (var fieldToRemove in GetColumnNamesFromField(field))
+ switch (field)
{
- list.Remove(fieldToRemove);
+ case ItemFields.Settings:
+ columns.Remove("IsLocked");
+ columns.Remove("PreferredMetadataCountryCode");
+ columns.Remove("PreferredMetadataLanguage");
+ columns.Remove("LockedFields");
+ break;
+ case ItemFields.ServiceName:
+ columns.Remove("ExternalServiceId");
+ break;
+ case ItemFields.SortName:
+ columns.Remove("ForcedSortName");
+ break;
+ case ItemFields.Taglines:
+ columns.Remove("Tagline");
+ break;
+ case ItemFields.Tags:
+ columns.Remove("Tags");
+ break;
+ case ItemFields.IsHD:
+ // do nothing
+ break;
+ default:
+ columns.Remove(field.ToString());
+ break;
}
}
}
if (!HasProgramAttributes(query))
{
- list.Remove("IsMovie");
- list.Remove("IsSeries");
- list.Remove("EpisodeTitle");
- list.Remove("IsRepeat");
- list.Remove("ShowId");
+ columns.Remove("IsMovie");
+ columns.Remove("IsSeries");
+ columns.Remove("EpisodeTitle");
+ columns.Remove("IsRepeat");
+ columns.Remove("ShowId");
}
if (!HasEpisodeAttributes(query))
{
- list.Remove("SeasonName");
- list.Remove("SeasonId");
+ columns.Remove("SeasonName");
+ columns.Remove("SeasonId");
}
if (!HasStartDate(query))
{
- list.Remove("StartDate");
+ columns.Remove("StartDate");
}
if (!HasTrailerTypes(query))
{
- list.Remove("TrailerTypes");
+ columns.Remove("TrailerTypes");
}
if (!HasArtistFields(query))
{
- list.Remove("AlbumArtists");
- list.Remove("Artists");
+ columns.Remove("AlbumArtists");
+ columns.Remove("Artists");
}
if (!HasSeriesFields(query))
{
- list.Remove("SeriesId");
+ columns.Remove("SeriesId");
}
if (!HasEpisodeAttributes(query))
{
- list.Remove("SeasonName");
- list.Remove("SeasonId");
+ columns.Remove("SeasonName");
+ columns.Remove("SeasonId");
}
if (!query.DtoOptions.EnableImages)
{
- list.Remove("Images");
+ columns.Remove("Images");
}
if (EnableJoinUserData(query))
{
- list.Add("UserDatas.UserId");
- list.Add("UserDatas.lastPlayedDate");
- list.Add("UserDatas.playbackPositionTicks");
- list.Add("UserDatas.playcount");
- list.Add("UserDatas.isFavorite");
- list.Add("UserDatas.played");
- list.Add("UserDatas.rating");
+ columns.Add("UserDatas.UserId");
+ columns.Add("UserDatas.lastPlayedDate");
+ columns.Add("UserDatas.playbackPositionTicks");
+ columns.Add("UserDatas.playcount");
+ columns.Add("UserDatas.isFavorite");
+ columns.Add("UserDatas.played");
+ columns.Add("UserDatas.rating");
}
if (query.SimilarTo != null)
@@ -2439,7 +2406,7 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SimilarityScore");
- list.Add(builder.ToString());
+ columns.Add(builder.ToString());
var oldLen = query.ExcludeItemIds.Length;
var newLen = oldLen + item.ExtraIds.Length + 1;
@@ -2466,10 +2433,8 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SearchScore");
- list.Add(builder.ToString());
+ columns.Add(builder.ToString());
}
-
- return list;
}
private void BindSearchParams(InternalItemsQuery query, IStatement statement)
@@ -2535,31 +2500,25 @@ namespace Emby.Server.Implementations.Data
private string GetGroupBy(InternalItemsQuery query)
{
- var groups = new List<string>();
-
- if (EnableGroupByPresentationUniqueKey(query))
+ var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query);
+ if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey)
{
- groups.Add("PresentationUniqueKey");
+ return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey";
}
- if (query.GroupBySeriesPresentationUniqueKey)
+ if (enableGroupByPresentationUniqueKey)
{
- groups.Add("SeriesPresentationUniqueKey");
+ return " Group by PresentationUniqueKey";
}
- if (groups.Count > 0)
+ if (query.GroupBySeriesPresentationUniqueKey)
{
- return " Group by " + string.Join(",", groups);
+ return " Group by SeriesPresentationUniqueKey";
}
return string.Empty;
}
- private string GetFromText(string alias = "A")
- {
- return " from TypedBaseItems " + alias;
- }
-
public int GetCount(InternalItemsQuery query)
{
if (query == null)
@@ -2577,17 +2536,21 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = new List<string> { "count(distinct PresentationUniqueKey)" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 256)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
+ var commandText = commandTextBuilder.ToString();
int count;
using (var connection = GetConnection(true))
{
@@ -2629,20 +2592,23 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = _retriveItemColumns.ToList();
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 1024)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -2650,15 +2616,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
+ var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>();
using (var connection = GetConnection(true))
{
@@ -2721,87 +2690,22 @@ namespace Emby.Server.Implementations.Data
private string FixUnicodeChars(string buffer)
{
- if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2013', '-'); // en dash
- }
-
- if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2014', '-'); // em dash
- }
-
- if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2015', '-'); // horizontal bar
- }
-
- if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2017', '_'); // double low line
- }
-
- if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
- }
-
- if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
- }
-
- if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
- }
-
- if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
- }
-
- if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
- }
-
- if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
- }
-
- if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
- }
-
- if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
- }
-
- if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2032', '\''); // prime
- }
-
- if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u2033', '\"'); // double prime
- }
-
- if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u0060', '\''); // grave accent
- }
-
- if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
- {
- buffer = buffer.Replace('\u00B4', '\''); // acute accent
- }
-
- return buffer;
+ buffer = buffer.Replace('\u2013', '-'); // en dash
+ buffer = buffer.Replace('\u2014', '-'); // em dash
+ buffer = buffer.Replace('\u2015', '-'); // horizontal bar
+ buffer = buffer.Replace('\u2017', '_'); // double low line
+ buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
+ buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
+ buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
+ buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
+ buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
+ buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
+ buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
+ buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
+ buffer = buffer.Replace('\u2032', '\''); // prime
+ buffer = buffer.Replace('\u2033', '\"'); // double prime
+ buffer = buffer.Replace('\u0060', '\''); // grave accent
+ return buffer.Replace('\u00B4', '\''); // acute accent
}
private void AddItem(List<BaseItem> items, BaseItem newItem)
@@ -2879,20 +2783,27 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = _retriveItemColumns.ToList();
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 512)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
var whereText = whereClauses.Count == 0 ?
string.Empty :
- " where " + string.Join(" AND ", whereClauses);
+ string.Join(" AND ", whereClauses);
- commandText += whereText
- + GetGroupBy(query)
- + GetOrderByText(query);
+ if (!string.IsNullOrEmpty(whereText))
+ {
+ commandTextBuilder.Append(" where ")
+ .Append(whereText);
+ }
+
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -2900,43 +2811,58 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
- var statementTexts = new List<string>();
+ var itemQuery = string.Empty;
+ var totalRecordCountQuery = string.Empty;
if (!isReturningZeroItems)
{
- statementTexts.Add(commandText);
+ itemQuery = commandTextBuilder.ToString();
}
if (query.EnableTotalRecordCount)
{
- commandText = string.Empty;
+ commandTextBuilder.Clear();
+ commandTextBuilder.Append(" select ");
+
+ List<string> columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (guid)" };
}
- commandText += GetJoinUserDataText(query)
- + whereText;
- statementTexts.Add(commandText);
+ SetFinalColumnsToSelect(query, columnsToSelect);
+
+ commandTextBuilder.AppendJoin(',', columnsToSelect)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
+ if (!string.IsNullOrEmpty(whereText))
+ {
+ commandTextBuilder.Append(" where ")
+ .Append(whereText);
+ }
+
+ totalRecordCountQuery = commandTextBuilder.ToString();
}
var list = new List<BaseItem>();
@@ -2946,11 +2872,12 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts);
+ var itemQueryStatement = PrepareStatement(db, itemQuery);
+ var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
if (!isReturningZeroItems)
{
- using (var statement = statements[0])
+ using (var statement = itemQueryStatement)
{
if (EnableJoinUserData(query))
{
@@ -2980,11 +2907,14 @@ namespace Emby.Server.Implementations.Data
}
}
}
+
+ LogQueryTime("GetItems.ItemQuery", itemQuery, now);
}
+ now = DateTime.UtcNow;
if (query.EnableTotalRecordCount)
{
- using (var statement = statements[statements.Length - 1])
+ using (var statement = totalRecordCountQueryStatement)
{
if (EnableJoinUserData(query))
{
@@ -2999,11 +2929,12 @@ namespace Emby.Server.Implementations.Data
result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
}
+
+ LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now);
}
}, ReadTransactionMode);
}
- LogQueryTime("GetItems", commandText, now);
result.Items = list;
return result;
}
@@ -3039,7 +2970,7 @@ namespace Emby.Server.Implementations.Data
return string.Empty;
}
- return " ORDER BY " + string.Join(",", orderBy.Select(i =>
+ return " ORDER BY " + string.Join(',', orderBy.Select(i =>
{
var columnMap = MapOrderByField(i.Item1, query);
@@ -3136,19 +3067,22 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = new List<string> { "guid" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 256)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -3156,15 +3090,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
+ var commandText = commandTextBuilder.ToString();
var list = new List<Guid>();
using (var connection = GetConnection(true))
{
@@ -3203,7 +3140,9 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
+ var columns = new List<string> { "guid", "path" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandText = "select " + string.Join(',', columns) + FromText;
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
@@ -3245,12 +3184,8 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery())
{
var id = row.GetGuid(0);
- string path = null;
- if (!row.IsDBNull(1))
- {
- path = row.GetString(1);
- }
+ row.TryGetString(1, out var path);
list.Add(new Tuple<Guid, string>(id, path));
}
@@ -3283,9 +3218,11 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
+ var columns = new List<string> { "guid" };
+ SetFinalColumnsToSelect(query, columns);
var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
- + GetFromText()
+ + string.Join(',', columns)
+ + FromText
+ GetJoinUserDataText(query);
var whereClauses = GetWhereClauses(query, null);
@@ -3325,19 +3262,23 @@ namespace Emby.Server.Implementations.Data
{
commandText = string.Empty;
+ List<string> columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (guid)" };
}
+ SetFinalColumnsToSelect(query, columnsToSelect);
+ commandText += " select " + string.Join(',', columnsToSelect) + FromText;
+
commandText += GetJoinUserDataText(query)
+ whereText;
statementTexts.Add(commandText);
@@ -3584,11 +3525,11 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@IsFolder", query.IsFolder);
}
- var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray();
+ var includeTypes = query.IncludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray();
// Only specify excluded types if no included types are specified
if (includeTypes.Length == 0)
{
- var excludeTypes = query.ExcludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray();
+ var excludeTypes = query.ExcludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray();
if (excludeTypes.Length == 1)
{
whereClauses.Add("type<>@type");
@@ -3596,7 +3537,7 @@ namespace Emby.Server.Implementations.Data
}
else if (excludeTypes.Length > 1)
{
- var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'"));
+ var inClause = string.Join(',', excludeTypes.Select(i => "'" + i + "'"));
whereClauses.Add($"type not in ({inClause})");
}
}
@@ -3607,7 +3548,7 @@ namespace Emby.Server.Implementations.Data
}
else if (includeTypes.Length > 1)
{
- var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'"));
+ var inClause = string.Join(',', includeTypes.Select(i => "'" + i + "'"));
whereClauses.Add($"type in ({inClause})");
}
@@ -3618,7 +3559,7 @@ namespace Emby.Server.Implementations.Data
}
else if (query.ChannelIds.Count > 1)
{
- var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+ var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add($"ChannelId in ({inClause})");
}
@@ -4351,7 +4292,7 @@ namespace Emby.Server.Implementations.Data
}
else if (query.Years.Length > 1)
{
- var val = string.Join(",", query.Years);
+ var val = string.Join(',', query.Years);
whereClauses.Add("ProductionYear in (" + val + ")");
}
@@ -4401,7 +4342,7 @@ namespace Emby.Server.Implementations.Data
}
else if (queryMediaTypes.Length > 1)
{
- var val = string.Join(",", queryMediaTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
whereClauses.Add("MediaType in (" + val + ")");
}
@@ -4444,7 +4385,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(string.Join(" AND ", excludeIds));
}
- if (query.ExcludeProviderIds.Count > 0)
+ if (query.ExcludeProviderIds != null && query.ExcludeProviderIds.Count > 0)
{
var excludeIds = new List<string>();
@@ -4474,7 +4415,7 @@ namespace Emby.Server.Implementations.Data
}
}
- if (query.HasAnyProviderId.Count > 0)
+ if (query.HasAnyProviderId != null && query.HasAnyProviderId.Count > 0)
{
var hasProviderIds = new List<string>();
@@ -4498,7 +4439,7 @@ namespace Emby.Server.Implementations.Data
var paramName = "@HasAnyProviderId" + index;
// this is a search for the placeholder
- hasProviderIds.Add("ProviderIds like " + paramName + "");
+ hasProviderIds.Add("ProviderIds like " + paramName);
// this replaces the placeholder with a value, here: %key=val%
if (statement != null)
@@ -4532,56 +4473,50 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
}
- var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList();
- var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
-
var queryTopParentIds = query.TopParentIds;
- if (queryTopParentIds.Length == 1)
+ if (queryTopParentIds.Length > 0)
{
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
+ var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
+ var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
+
+ if (queryTopParentIds.Length == 1)
{
- whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
- if (statement != null)
+ if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
- statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
+ statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ }
+ else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ {
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
+ whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
+ }
+ else
+ {
+ whereClauses.Add("(TopParentId=@TopParentId)");
}
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
- }
- else
- {
- whereClauses.Add("(TopParentId=@TopParentId)");
- }
- if (statement != null)
- {
- statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
+ statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
}
- }
- else if (queryTopParentIds.Length > 1)
- {
- var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
-
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
+ else if (queryTopParentIds.Length > 1)
{
- whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
- if (statement != null)
+ var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+
+ if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
- statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
+ statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ }
+ else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ {
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
+ whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
+ }
+ else
+ {
+ whereClauses.Add("TopParentId in (" + val + ")");
}
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
- }
- else
- {
- whereClauses.Add("TopParentId in (" + val + ")");
}
}
@@ -4597,7 +4532,7 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length > 1)
{
- var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+ var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
}
@@ -4790,27 +4725,27 @@ namespace Emby.Server.Implementations.Data
if (IsTypeInQuery(nameof(Person), query))
{
- list.Add(nameof(Person));
+ list.Add(typeof(Person).FullName);
}
if (IsTypeInQuery(nameof(Genre), query))
{
- list.Add(nameof(Genre));
+ list.Add(typeof(Genre).FullName);
}
if (IsTypeInQuery(nameof(MusicGenre), query))
{
- list.Add(nameof(MusicGenre));
+ list.Add(typeof(MusicGenre).FullName);
}
if (IsTypeInQuery(nameof(MusicArtist), query))
{
- list.Add(nameof(MusicArtist));
+ list.Add(typeof(MusicArtist).FullName);
}
if (IsTypeInQuery(nameof(Studio), query))
{
- list.Add(nameof(Studio));
+ list.Add(typeof(Studio).FullName);
}
return list;
@@ -4863,17 +4798,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- var types = new[]
- {
- nameof(Episode),
- nameof(Video),
- nameof(Movie),
- nameof(MusicVideo),
- nameof(Series),
- nameof(Season)
- };
-
- if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase))
{
return true;
}
@@ -4915,15 +4845,10 @@ namespace Emby.Server.Implementations.Data
typeof(AggregateFolder)
};
- public void UpdateInheritedValues(CancellationToken cancellationToken)
- {
- UpdateInheritedTags(cancellationToken);
- }
-
- private void UpdateInheritedTags(CancellationToken cancellationToken)
+ public void UpdateInheritedValues()
{
string sql = string.Join(
- ";",
+ ';',
new string[]
{
"delete from itemvalues where type = 6",
@@ -4946,37 +4871,38 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
- private static Dictionary<string, string[]> GetTypeMapDictionary()
+ private static Dictionary<string, string> GetTypeMapDictionary()
{
- var dict = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
+ var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var t in _knownTypes)
{
- dict[t.Name] = new[] { t.FullName };
+ dict[t.Name] = t.FullName ;
}
- dict["Program"] = new[] { typeof(LiveTvProgram).FullName };
- dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName };
+ dict["Program"] = typeof(LiveTvProgram).FullName;
+ dict["TvChannel"] = typeof(LiveTvChannel).FullName;
return dict;
}
// Not crazy about having this all the way down here, but at least it's in one place
- private readonly Dictionary<string, string[]> _types = GetTypeMapDictionary();
+ private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
- private string[] MapIncludeItemTypes(string value)
+ private string MapIncludeItemTypes(string value)
{
- if (_types.TryGetValue(value, out string[] result))
+ if (_types.TryGetValue(value, out string result))
{
return result;
}
if (IsValidType(value))
{
- return new[] { value };
+ return value;
}
- return Array.Empty<string>();
+ Logger.LogWarning("Unknown item type: {ItemType}", value);
+ return null;
}
public void DeleteItem(Guid id)
@@ -5148,7 +5074,7 @@ AND Type = @InternalPersonType)");
}
else if (queryPersonTypes.Count > 1)
{
- var val = string.Join(",", queryPersonTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'"));
whereClauses.Add("PersonType in (" + val + ")");
}
@@ -5162,7 +5088,7 @@ AND Type = @InternalPersonType)");
}
else if (queryExcludePersonTypes.Count > 1)
{
- var val = string.Join(",", queryExcludePersonTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'"));
whereClauses.Add("PersonType not in (" + val + ")");
}
@@ -5279,64 +5205,87 @@ AND Type = @InternalPersonType)");
public List<string> GetStudioNames()
{
- return GetItemValueNames(new[] { 3 }, new List<string>(), new List<string>());
+ return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>());
}
public List<string> GetAllArtistNames()
{
- return GetItemValueNames(new[] { 0, 1 }, new List<string>(), new List<string>());
+ return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>());
}
public List<string> GetMusicGenreNames()
{
- return GetItemValueNames(new[] { 2 }, new List<string> { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }, new List<string>());
+ return GetItemValueNames(
+ new[] { 2 },
+ new string[]
+ {
+ typeof(Audio).FullName,
+ typeof(MusicVideo).FullName,
+ typeof(MusicAlbum).FullName,
+ typeof(MusicArtist).FullName
+ },
+ Array.Empty<string>());
}
public List<string> GetGenreNames()
{
- return GetItemValueNames(new[] { 2 }, new List<string>(), new List<string> { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" });
+ return GetItemValueNames(
+ new[] { 2 },
+ Array.Empty<string>(),
+ new string[]
+ {
+ typeof(Audio).FullName,
+ typeof(MusicVideo).FullName,
+ typeof(MusicAlbum).FullName,
+ typeof(MusicArtist).FullName
+ });
}
- private List<string> GetItemValueNames(int[] itemValueTypes, List<string> withItemTypes, List<string> excludeItemTypes)
+ private List<string> GetItemValueNames(int[] itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
{
CheckDisposed();
- withItemTypes = withItemTypes.SelectMany(MapIncludeItemTypes).ToList();
- excludeItemTypes = excludeItemTypes.SelectMany(MapIncludeItemTypes).ToList();
-
var now = DateTime.UtcNow;
- var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
-
- var commandText = "Select Value From ItemValues where " + typeClause;
+ var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
+ if (itemValueTypes.Length == 1)
+ {
+ stringBuilder.Append('=')
+ .Append(itemValueTypes[0]);
+ }
+ else
+ {
+ stringBuilder.Append(" in (")
+ .AppendJoin(',', itemValueTypes)
+ .Append(')');
+ }
if (withItemTypes.Count > 0)
{
- var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'"));
- commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
+ stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (")
+ .AppendJoinInSingleQuotes(',', withItemTypes)
+ .Append("))");
}
if (excludeItemTypes.Count > 0)
{
- var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'"));
- commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
+ stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (")
+ .AppendJoinInSingleQuotes(',', excludeItemTypes)
+ .Append("))");
}
- commandText += " Group By CleanValue";
+ stringBuilder.Append(" Group By CleanValue");
+ var commandText = stringBuilder.ToString();
var list = new List<string>();
using (var connection = GetConnection(true))
+ using (var statement = PrepareStatement(connection, commandText))
{
- using (var statement = PrepareStatement(connection, commandText))
+ foreach (var row in statement.ExecuteQuery())
{
- foreach (var row in statement.ExecuteQuery())
+ if (row.TryGetString(0, out var result))
{
- if (!row.IsDBNull(0))
- {
- list.Add(row.GetString(0));
- }
+ list.Add(result);
}
}
}
@@ -5362,18 +5311,19 @@ AND Type = @InternalPersonType)");
var now = DateTime.UtcNow;
var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+ ("Type=" + itemValueTypes[0]) :
+ ("Type in (" + string.Join(',', itemValueTypes) + ")");
InternalItemsQuery typeSubQuery = null;
- Dictionary<string, string> itemCountColumns = null;
+ string itemCountColumns = null;
+ var stringBuilder = new StringBuilder(1024);
var typesToCount = query.IncludeItemTypes;
if (typesToCount.Length > 0)
{
- var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B");
+ stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B");
typeSubQuery = new InternalItemsQuery(query.User)
{
@@ -5389,20 +5339,22 @@ AND Type = @InternalPersonType)");
};
var whereClauses = GetWhereClauses(typeSubQuery, null);
- whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")");
+ stringBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses)
+ .Append(" AND ")
+ .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ")
+ .Append(typeClause)
+ .Append(")) as itemTypes");
- itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses);
-
- itemCountColumns = new Dictionary<string, string>()
- {
- { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
- };
+ itemCountColumns = stringBuilder.ToString();
+ stringBuilder.Clear();
}
List<string> columns = _retriveItemColumns.ToList();
- if (itemCountColumns != null)
+ // Unfortunately we need to add it to columns to ensure the order of the columns in the select
+ if (!string.IsNullOrEmpty(itemCountColumns))
{
- columns.AddRange(itemCountColumns.Values);
+ columns.Add(itemCountColumns);
}
// do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo
@@ -5415,7 +5367,6 @@ AND Type = @InternalPersonType)");
ItemIds = query.ItemIds,
TopParentIds = query.TopParentIds,
ParentId = query.ParentId,
- IsPlayed = query.IsPlayed,
IsAiring = query.IsAiring,
IsMovie = query.IsMovie,
IsSports = query.IsSports,
@@ -5424,23 +5375,24 @@ AND Type = @InternalPersonType)");
IsSeries = query.IsSeries
};
- columns = GetFinalColumnsToSelect(query, columns);
-
- var commandText = "select "
- + string.Join(",", columns)
- + GetFromText()
- + GetJoinUserDataText(query);
+ SetFinalColumnsToSelect(query, columns);
var innerWhereClauses = GetWhereClauses(innerQuery, null);
- var innerWhereText = innerWhereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", innerWhereClauses);
+ stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ")
+ .Append(typeClause)
+ .Append(" AND ItemId in (select guid from TypedBaseItems");
+ if (innerWhereClauses.Count > 0)
+ {
+ stringBuilder.Append(" where ")
+ .AppendJoin(" AND ", innerWhereClauses);
+ }
- var whereText = " where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))";
+ stringBuilder.Append("))");
var outerQuery = new InternalItemsQuery(query.User)
{
+ IsPlayed = query.IsPlayed,
IsFavorite = query.IsFavorite,
IsFavoriteOrLiked = query.IsFavoriteOrLiked,
IsLiked = query.IsLiked,
@@ -5461,21 +5413,31 @@ AND Type = @InternalPersonType)");
};
var outerWhereClauses = GetWhereClauses(outerQuery, null);
-
if (outerWhereClauses.Count != 0)
{
- whereText += " AND " + string.Join(" AND ", outerWhereClauses);
+ stringBuilder.Append(" AND ")
+ .AppendJoin(" AND ", outerWhereClauses);
}
- commandText += whereText + " group by PresentationUniqueKey";
+ var whereText = stringBuilder.ToString();
+ stringBuilder.Clear();
+
+ stringBuilder.Append("select ")
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query))
+ .Append(whereText)
+ .Append(" group by PresentationUniqueKey");
- if (query.SimilarTo != null || !string.IsNullOrEmpty(query.SearchTerm))
+ if (query.OrderBy.Count != 0
+ || query.SimilarTo != null
+ || !string.IsNullOrEmpty(query.SearchTerm))
{
- commandText += GetOrderByText(query);
+ stringBuilder.Append(GetOrderByText(query));
}
else
{
- commandText += " order by SortName";
+ stringBuilder.Append(" order by SortName");
}
if (query.Limit.HasValue || query.StartIndex.HasValue)
@@ -5484,32 +5446,39 @@ AND Type = @InternalPersonType)");
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ stringBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ stringBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
- var statementTexts = new List<string>();
+ string commandText = string.Empty;
+
if (!isReturningZeroItems)
{
- statementTexts.Add(commandText);
+ commandText = stringBuilder.ToString();
}
+ string countText = string.Empty;
if (query.EnableTotalRecordCount)
{
- var countText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
+ stringBuilder.Clear();
+ var columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
+ SetFinalColumnsToSelect(query, columnsToSelect);
+ stringBuilder.Append("select ")
+ .AppendJoin(',', columnsToSelect)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query))
+ .Append(whereText);
- statementTexts.Add(countText);
+ countText = stringBuilder.ToString();
}
var list = new List<(BaseItem, ItemCounts)>();
@@ -5519,11 +5488,9 @@ AND Type = @InternalPersonType)");
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts);
-
if (!isReturningZeroItems)
{
- using (var statement = statements[0])
+ using (var statement = PrepareStatement(db, commandText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5564,13 +5531,7 @@ AND Type = @InternalPersonType)");
if (query.EnableTotalRecordCount)
{
- commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
-
- using (var statement = statements[statements.Length - 1])
+ using (var statement = PrepareStatement(db, countText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5607,7 +5568,7 @@ AND Type = @InternalPersonType)");
return result;
}
- private ItemCounts GetItemCounts(IReadOnlyList<IResultSetValue> reader, int countStartColumn, string[] typesToCount)
+ private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, string[] typesToCount)
{
var counts = new ItemCounts();
@@ -5616,51 +5577,43 @@ AND Type = @InternalPersonType)");
return counts;
}
- var typeString = reader.IsDBNull(countStartColumn) ? null : reader.GetString(countStartColumn);
-
- if (string.IsNullOrWhiteSpace(typeString))
+ if (!reader.TryGetString(countStartColumn, out var typeString))
{
return counts;
}
- var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries)
- .ToLookup(x => x);
-
- foreach (var type in allTypes)
+ foreach (var typeName in typeString.AsSpan().Split('|'))
{
- var value = type.Count();
- var typeName = type.Key;
-
- if (string.Equals(typeName, typeof(Series).FullName, StringComparison.OrdinalIgnoreCase))
+ if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.SeriesCount = value;
+ counts.SeriesCount++;
}
- else if (string.Equals(typeName, typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.EpisodeCount = value;
+ counts.EpisodeCount++;
}
- else if (string.Equals(typeName, typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.MovieCount = value;
+ counts.MovieCount++;
}
- else if (string.Equals(typeName, typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.AlbumCount = value;
+ counts.AlbumCount++;
}
- else if (string.Equals(typeName, typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.ArtistCount = value;
+ counts.ArtistCount++;
}
- else if (string.Equals(typeName, typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.SongCount = value;
+ counts.SongCount++;
}
- else if (string.Equals(typeName, typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase))
+ else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase))
{
- counts.TrailerCount = value;
+ counts.TrailerCount++;
}
- counts.ItemCount += value;
+ counts.ItemCount++;
}
return counts;
@@ -5809,7 +5762,10 @@ AND Type = @InternalPersonType)");
var endIndex = Math.Min(people.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
{
- insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", i.ToString(CultureInfo.InvariantCulture));
+ insertText.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),",
+ i.ToString(CultureInfo.InvariantCulture));
}
// Remove last comma
@@ -5843,7 +5799,7 @@ AND Type = @InternalPersonType)");
}
}
- private PersonInfo GetPerson(IReadOnlyList<IResultSetValue> reader)
+ private PersonInfo GetPerson(IReadOnlyList<ResultSetValue> reader)
{
var item = new PersonInfo
{
@@ -5851,19 +5807,19 @@ AND Type = @InternalPersonType)");
Name = reader.GetString(1)
};
- if (!reader.IsDBNull(2))
+ if (reader.TryGetString(2, out var role))
{
- item.Role = reader.GetString(2);
+ item.Role = role;
}
- if (!reader.IsDBNull(3))
+ if (reader.TryGetString(3, out var type))
{
- item.Type = reader.GetString(3);
+ item.Type = type;
}
- if (!reader.IsDBNull(4))
+ if (reader.TryGetInt32(4, out var sortOrder))
{
- item.SortOrder = reader.GetInt32(4);
+ item.SortOrder = sortOrder;
}
return item;
@@ -6050,7 +6006,7 @@ AND Type = @InternalPersonType)");
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>ChapterInfo.</returns>
- private MediaStream GetMediaStream(IReadOnlyList<IResultSetValue> reader)
+ private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
{
var item = new MediaStream
{
@@ -6059,157 +6015,157 @@ AND Type = @InternalPersonType)");
item.Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true);
- if (reader[3].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(3, out var codec))
{
- item.Codec = reader[3].ToString();
+ item.Codec = codec;
}
- if (reader[4].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(4, out var language))
{
- item.Language = reader[4].ToString();
+ item.Language = language;
}
- if (reader[5].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(5, out var channelLayout))
{
- item.ChannelLayout = reader[5].ToString();
+ item.ChannelLayout = channelLayout;
}
- if (reader[6].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(6, out var profile))
{
- item.Profile = reader[6].ToString();
+ item.Profile = profile;
}
- if (reader[7].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(7, out var aspectRatio))
{
- item.AspectRatio = reader[7].ToString();
+ item.AspectRatio = aspectRatio;
}
- if (reader[8].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(8, out var path))
{
- item.Path = RestorePath(reader[8].ToString());
+ item.Path = RestorePath(path);
}
item.IsInterlaced = reader.GetBoolean(9);
- if (reader[10].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(10, out var bitrate))
{
- item.BitRate = reader.GetInt32(10);
+ item.BitRate = bitrate;
}
- if (reader[11].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(11, out var channels))
{
- item.Channels = reader.GetInt32(11);
+ item.Channels = channels;
}
- if (reader[12].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(12, out var sampleRate))
{
- item.SampleRate = reader.GetInt32(12);
+ item.SampleRate = sampleRate;
}
item.IsDefault = reader.GetBoolean(13);
item.IsForced = reader.GetBoolean(14);
item.IsExternal = reader.GetBoolean(15);
- if (reader[16].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(16, out var width))
{
- item.Width = reader.GetInt32(16);
+ item.Width = width;
}
- if (reader[17].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(17, out var height))
{
- item.Height = reader.GetInt32(17);
+ item.Height = height;
}
- if (reader[18].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetSingle(18, out var averageFrameRate))
{
- item.AverageFrameRate = reader.GetFloat(18);
+ item.AverageFrameRate = averageFrameRate;
}
- if (reader[19].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetSingle(19, out var realFrameRate))
{
- item.RealFrameRate = reader.GetFloat(19);
+ item.RealFrameRate = realFrameRate;
}
- if (reader[20].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetSingle(20, out var level))
{
- item.Level = reader.GetFloat(20);
+ item.Level = level;
}
- if (reader[21].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(21, out var pixelFormat))
{
- item.PixelFormat = reader[21].ToString();
+ item.PixelFormat = pixelFormat;
}
- if (reader[22].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(22, out var bitDepth))
{
- item.BitDepth = reader.GetInt32(22);
+ item.BitDepth = bitDepth;
}
- if (reader[23].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetBoolean(23, out var isAnamorphic))
{
- item.IsAnamorphic = reader.GetBoolean(23);
+ item.IsAnamorphic = isAnamorphic;
}
- if (reader[24].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(24, out var refFrames))
{
- item.RefFrames = reader.GetInt32(24);
+ item.RefFrames = refFrames;
}
- if (reader[25].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(25, out var codecTag))
{
- item.CodecTag = reader.GetString(25);
+ item.CodecTag = codecTag;
}
- if (reader[26].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(26, out var comment))
{
- item.Comment = reader.GetString(26);
+ item.Comment = comment;
}
- if (reader[27].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(27, out var nalLengthSize))
{
- item.NalLengthSize = reader.GetString(27);
+ item.NalLengthSize = nalLengthSize;
}
- if (reader[28].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetBoolean(28, out var isAVC))
{
- item.IsAVC = reader[28].ToBool();
+ item.IsAVC = isAVC;
}
- if (reader[29].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(29, out var title))
{
- item.Title = reader[29].ToString();
+ item.Title = title;
}
- if (reader[30].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(30, out var timeBase))
{
- item.TimeBase = reader[30].ToString();
+ item.TimeBase = timeBase;
}
- if (reader[31].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(31, out var codecTimeBase))
{
- item.CodecTimeBase = reader[31].ToString();
+ item.CodecTimeBase = codecTimeBase;
}
- if (reader[32].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(32, out var colorPrimaries))
{
- item.ColorPrimaries = reader[32].ToString();
+ item.ColorPrimaries = colorPrimaries;
}
- if (reader[33].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(33, out var colorSpace))
{
- item.ColorSpace = reader[33].ToString();
+ item.ColorSpace = colorSpace;
}
- if (reader[34].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(34, out var colorTransfer))
{
- item.ColorTransfer = reader[34].ToString();
+ item.ColorTransfer = colorTransfer;
}
if (item.Type == MediaStreamType.Subtitle)
{
- item.localizedUndefined = _localization.GetLocalizedString("Undefined");
- item.localizedDefault = _localization.GetLocalizedString("Default");
- item.localizedForced = _localization.GetLocalizedString("Forced");
+ item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
+ item.LocalizedDefault = _localization.GetLocalizedString("Default");
+ item.LocalizedForced = _localization.GetLocalizedString("Forced");
}
return item;
@@ -6261,7 +6217,7 @@ AND Type = @InternalPersonType)");
CheckDisposed();
if (id == Guid.Empty)
{
- throw new ArgumentException(nameof(id));
+ throw new ArgumentException("Guid can't be empty.", nameof(id));
}
if (attachments == null)
@@ -6351,36 +6307,36 @@ AND Type = @InternalPersonType)");
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>MediaAttachment.</returns>
- private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader)
+ private MediaAttachment GetMediaAttachment(IReadOnlyList<ResultSetValue> reader)
{
var item = new MediaAttachment
{
Index = reader[1].ToInt()
};
- if (reader[2].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(2, out var codec))
{
- item.Codec = reader[2].ToString();
+ item.Codec = codec;
}
- if (reader[2].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(3, out var codecTag))
{
- item.CodecTag = reader[3].ToString();
+ item.CodecTag = codecTag;
}
- if (reader[4].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(4, out var comment))
{
- item.Comment = reader[4].ToString();
+ item.Comment = comment;
}
- if (reader[6].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(5, out var fileName))
{
- item.FileName = reader[5].ToString();
+ item.FileName = fileName;
}
- if (reader[6].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(6, out var mimeType))
{
- item.MimeType = reader[6].ToString();
+ item.MimeType = mimeType;
}
return item;
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 2c4e8e0fc..ef9af1dcd 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- db.ExecuteAll(string.Join(";", new[] {
+ db.ExecuteAll(string.Join(';', new[] {
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
@@ -348,16 +350,16 @@ namespace Emby.Server.Implementations.Data
/// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader"></param>
- private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
+ private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
{
var userData = new UserItemData();
userData.Key = reader[0].ToString();
// userData.UserId = reader[1].ReadGuidFromBlob();
- if (reader[2].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetDouble(2, out var rating))
{
- userData.Rating = reader[2].ToDouble();
+ userData.Rating = rating;
}
userData.Played = reader[3].ToBool();
@@ -365,19 +367,19 @@ namespace Emby.Server.Implementations.Data
userData.IsFavorite = reader[5].ToBool();
userData.PlaybackPositionTicks = reader[6].ToInt64();
- if (reader[7].SQLiteType != SQLiteType.Null)
+ if (reader.TryReadDateTime(7, out var lastPlayedDate))
{
- userData.LastPlayedDate = reader[7].TryReadDateTime();
+ userData.LastPlayedDate = lastPlayedDate;
}
- if (reader[8].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(8, out var audioStreamIndex))
{
- userData.AudioStreamIndex = reader[8].ToInt();
+ userData.AudioStreamIndex = audioStreamIndex;
}
- if (reader[9].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetInt32(9, out var subtitleStreamIndex))
{
- userData.SubtitleStreamIndex = reader[9].ToInt();
+ userData.SubtitleStreamIndex = subtitleStreamIndex;
}
return userData;
diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs
index 7044b1d19..064664e1f 100644
--- a/Emby.Server.Implementations/Data/TypeMapper.cs
+++ b/Emby.Server.Implementations/Data/TypeMapper.cs
@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data
/// This holds all the types in the running assemblies
/// so that we can de-serialize properly when we don't have strong types.
/// </summary>
- private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
+ private readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
/// <summary>
/// Gets the type.
@@ -21,26 +21,16 @@ namespace Emby.Server.Implementations.Data
/// <param name="typeName">Name of the type.</param>
/// <returns>Type.</returns>
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
- public Type GetType(string typeName)
+ public Type? GetType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
throw new ArgumentNullException(nameof(typeName));
}
- return _typeMap.GetOrAdd(typeName, LookupType);
- }
-
- /// <summary>
- /// Lookups the type.
- /// </summary>
- /// <param name="typeName">Name of the type.</param>
- /// <returns>Type.</returns>
- private Type LookupType(string typeName)
- {
- return AppDomain.CurrentDomain.GetAssemblies()
- .Select(a => a.GetType(typeName))
- .FirstOrDefault(t => t != null);
+ return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
+ .Select(a => a.GetType(k))
+ .FirstOrDefault(t => t != null));
}
}
}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index fa6ac95fd..3d15b3e76 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index da5047d24..2637addce 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 54b18a8c8..7411239a1 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -665,10 +667,7 @@ namespace Emby.Server.Implementations.Dto
var tag = GetImageCacheTag(item, image);
if (!string.IsNullOrEmpty(image.BlurHash))
{
- if (dto.ImageBlurHashes == null)
- {
- dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
- }
+ dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
if (!dto.ImageBlurHashes.ContainsKey(image.Type))
{
@@ -702,10 +701,7 @@ namespace Emby.Server.Implementations.Dto
if (hashes.Count > 0)
{
- if (dto.ImageBlurHashes == null)
- {
- dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
- }
+ dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
dto.ImageBlurHashes[imageType] = hashes;
}
@@ -898,10 +894,7 @@ namespace Emby.Server.Implementations.Dto
dto.Taglines = new string[] { item.Tagline };
}
- if (dto.Taglines == null)
- {
- dto.Taglines = Array.Empty<string>();
- }
+ dto.Taglines ??= Array.Empty<string>();
}
dto.Type = item.GetBaseItemKind();
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 08047ba47..9c90de1ed 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -9,6 +9,7 @@
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
+ <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
@@ -27,10 +28,11 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
- <PackageReference Include="sharpcompress" Version="0.27.1" />
- <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
+ <PackageReference Include="sharpcompress" Version="0.28.3" />
+ <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
@@ -43,22 +45,20 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
+ <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" />
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 14201ead2..0a4efd73c 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.StartDiscovery();
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
-
- _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
}
private void Stop()
@@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
_timer?.Dispose();
-
- _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
- }
-
- private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
- {
- NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
}
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index ae1b51b4c..5bb4100ba 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index 824bb85f4..e0ca02d98 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 9486874d5..2e72b18f5 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,3 +1,4 @@
+using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
@@ -29,7 +30,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// The UDP server.
/// </summary>
- private UdpServer _udpServer;
+ private UdpServer? _udpServer;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed = false;
@@ -49,10 +50,12 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc />
public Task RunAsync()
{
+ CheckDisposed();
+
try
{
- _udpServer = new UdpServer(_logger, _appHost, _config);
- _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+ _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
+ _udpServer.Start(_cancellationTokenSource.Token);
}
catch (SocketException ex)
{
@@ -62,6 +65,14 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(this.GetType().Name);
+ }
+ }
+
/// <inheritdoc />
public void Dispose()
{
@@ -71,9 +82,8 @@ namespace Emby.Server.Implementations.EntryPoints
}
_cancellationTokenSource.Cancel();
- _udpServer.Dispose();
_cancellationTokenSource.Dispose();
- _cancellationTokenSource = null;
+ _udpServer?.Dispose();
_udpServer = null;
_disposed = true;
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 1989e9ed2..d3bcd5e13 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
private readonly object _syncLock = new object();
- private Timer _updateTimer;
+ private Timer? _updateTimer;
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
{
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
- void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
+ private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
{
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
{
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
}
- if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
+ if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
{
keys = new List<BaseItem>();
_changedItems[e.UserId] = keys;
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- private void UpdateTimerCallback(object state)
+ private void UpdateTimerCallback(object? state)
{
lock (_syncLock)
{
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 4a0fc8239..9afabf527 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,6 +1,5 @@
#pragma warning disable CS1591
-using System;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 024404ceb..c87f7dbbd 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -2,8 +2,8 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Net;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
- return (AuthorizationInfo)cached;
+ return (AuthorizationInfo)cached!; // Cache should never contain null
}
return GetAuthorization(requestContext);
@@ -55,15 +55,15 @@ namespace Emby.Server.Implementations.HttpServer.Security
}
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
- in Dictionary<string, string> auth,
+ in Dictionary<string, string>? auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
{
- string deviceId = null;
- string device = null;
- string client = null;
- string version = null;
- string token = null;
+ string? deviceId = null;
+ string? device = null;
+ string? client = null;
+ string? version = null;
+ string? token = null;
if (auth != null)
{
@@ -206,7 +206,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
+ private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
@@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
- return GetAuthorization(auth);
+ return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
/// <summary>
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
+ private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
@@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Headers[HeaderNames.Authorization];
}
- return GetAuthorization(auth);
+ return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
/// <summary>
@@ -240,43 +240,43 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string> GetAuthorization(string authorizationHeader)
+ private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
{
if (authorizationHeader == null)
{
return null;
}
- var parts = authorizationHeader.Split(' ', 2);
+ var firstSpace = authorizationHeader.IndexOf(' ');
- // There should be at least to parts
- if (parts.Length != 2)
+ // There should be at least two parts
+ if (firstSpace == -1)
{
return null;
}
- var acceptedNames = new[] { "MediaBrowser", "Emby" };
+ var name = authorizationHeader[..firstSpace];
- // It has to be a digest request
- if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
+ if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
+ && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
{
return null;
}
- // Remove uptil the first space
- authorizationHeader = parts[1];
- parts = authorizationHeader.Split(',');
+ authorizationHeader = authorizationHeader[(firstSpace + 1)..];
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- foreach (var item in parts)
+ foreach (var item in authorizationHeader.Split(','))
{
- var param = item.Trim().Split('=', 2);
+ var trimmedItem = item.Trim();
+ var firstEqualsSign = trimmedItem.IndexOf('=');
- if (param.Length == 2)
+ if (firstEqualsSign > 0)
{
- var value = NormalizeValue(param[1].Trim('"'));
- result[param[0]] = value;
+ var key = trimmedItem[..firstEqualsSign].ToString();
+ var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
+ result[key] = value;
}
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 040b6b9e4..c375f36ce 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
- return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
+ return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
}
public SessionInfo GetSession(object requestContext)
@@ -36,14 +36,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetSession((HttpContext)requestContext);
}
- public User GetUser(HttpContext requestContext)
+ public User? GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
- public User GetUser(object requestContext)
+ public User? GetUser(object requestContext)
{
return GetUser(((HttpRequest)requestContext).HttpContext);
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7e0c2c1da..8f7d60669 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Buffers;
using System.IO.Pipelines;
@@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint;
QueryString = query;
- _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.Now;
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index d6cf6233e..861c0a95e 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -14,15 +16,18 @@ namespace Emby.Server.Implementations.HttpServer
public class WebSocketManager : IWebSocketManager
{
private readonly IWebSocketListener[] _webSocketListeners;
+ private readonly IAuthService _authService;
private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory;
public WebSocketManager(
+ IAuthService authService,
IEnumerable<IWebSocketListener> webSocketListeners,
ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory)
{
_webSocketListeners = webSocketListeners.ToArray();
+ _authService = authService;
_logger = logger;
_loggerFactory = loggerFactory;
}
@@ -30,6 +35,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
+ _ = _authService.Authenticate(context.Request);
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 7435e9d0b..47a83d77c 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 3353fae9d..aa80bccd7 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index c0e757543..64d802457 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -2,12 +2,12 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Text;
+using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
- private readonly bool _isEnvironmentCaseInsensitive;
+ private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
@@ -32,8 +32,6 @@ namespace Emby.Server.Implementations.IO
{
Logger = logger;
_tempPath = applicationPaths.TempDirectory;
-
- _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
}
public virtual void AddShortcutHandler(IShortcutHandler handler)
@@ -64,7 +62,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">filename</exception>
- public virtual string ResolveShortcut(string filename)
+ public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
{
@@ -72,7 +70,7 @@ namespace Emby.Server.Implementations.IO
}
var extension = Path.GetExtension(filename);
- var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
+ var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
return handler?.Resolve(filename);
}
@@ -246,16 +244,23 @@ namespace Emby.Server.Implementations.IO
{
result.Length = fileInfo.Length;
- // Issue #2354 get the size of files behind symbolic links
- if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+ // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
+ if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{
- using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ try
{
- result.Length = thisFileStream.Length;
+ using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ {
+ result.Length = thisFileStream.Length;
+ }
+ }
+ catch (FileNotFoundException ex)
+ {
+ // Dangling symlinks cannot be detected before opening the file unfortunately...
+ Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
+ result.Exists = false;
}
}
-
- result.DirectoryName = fileInfo.DirectoryName;
}
result.CreationTimeUtc = GetCreationTimeUtc(info);
@@ -294,16 +299,37 @@ namespace Emby.Server.Implementations.IO
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">The filename is null.</exception>
- public virtual string GetValidFilename(string filename)
+ public string GetValidFilename(string filename)
{
- var builder = new StringBuilder(filename);
-
- foreach (var c in Path.GetInvalidFileNameChars())
+ var invalid = Path.GetInvalidFileNameChars();
+ var first = filename.IndexOfAny(invalid);
+ if (first == -1)
{
- builder = builder.Replace(c, ' ');
+ // Fast path for clean strings
+ return filename;
}
- return builder.ToString();
+ return string.Create(
+ filename.Length,
+ (filename, invalid, first),
+ (chars, state) =>
+ {
+ state.filename.AsSpan().CopyTo(chars);
+
+ chars[state.first++] = ' ';
+
+ var len = chars.Length;
+ foreach (var c in state.invalid)
+ {
+ for (int i = state.first; i < len; i++)
+ {
+ if (chars[i] == c)
+ {
+ chars[i] = ' ';
+ }
+ }
+ }
+ });
}
/// <summary>
@@ -487,26 +513,9 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path));
}
- var separatorChar = Path.DirectorySeparatorChar;
-
- return path.IndexOf(parentPath.TrimEnd(separatorChar) + separatorChar, StringComparison.OrdinalIgnoreCase) != -1;
- }
-
- public virtual bool IsRootPath(string path)
- {
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- var parent = Path.GetDirectoryName(path);
-
- if (!string.IsNullOrEmpty(parent))
- {
- return false;
- }
-
- return true;
+ return path.Contains(
+ Path.TrimEndingDirectorySeparator(parentPath) + Path.DirectorySeparatorChar,
+ _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
public virtual string NormalizePath(string path)
@@ -521,7 +530,7 @@ namespace Emby.Server.Implementations.IO
return path;
}
- return path.TrimEnd(Path.DirectorySeparatorChar);
+ return Path.TrimEndingDirectorySeparator(path);
}
public virtual bool AreEqual(string path1, string path2)
@@ -536,7 +545,10 @@ namespace Emby.Server.Implementations.IO
return false;
}
- return string.Equals(NormalizePath(path1), NormalizePath(path2), StringComparison.OrdinalIgnoreCase);
+ return string.Equals(
+ NormalizePath(path1),
+ NormalizePath(path2),
+ _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
@@ -590,7 +602,7 @@ namespace Emby.Server.Implementations.IO
return GetFiles(path, null, false, recursive);
}
- public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+ public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -607,13 +619,13 @@ namespace Emby.Server.Implementations.IO
{
files = files.Where(i =>
{
- var ext = i.Extension;
- if (ext == null)
+ var ext = i.Extension.AsSpan();
+ if (ext.IsEmpty)
{
return false;
}
- return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+ return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
@@ -625,8 +637,7 @@ namespace Emby.Server.Implementations.IO
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
- return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
- .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
+ return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions));
}
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -644,7 +655,7 @@ namespace Emby.Server.Implementations.IO
return GetFilePaths(path, null, false, recursive);
}
- public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+ public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -661,13 +672,13 @@ namespace Emby.Server.Implementations.IO
{
files = files.Where(i =>
{
- var ext = Path.GetExtension(i);
- if (ext == null)
+ var ext = Path.GetExtension(i.AsSpan());
+ if (ext.IsEmpty)
{
return false;
}
- return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+ return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
@@ -689,20 +700,5 @@ namespace Emby.Server.Implementations.IO
AttributesToSkip = 0
};
}
-
- private static void RunProcess(string path, string args, string workingDirectory)
- {
- using (var process = Process.Start(new ProcessStartInfo
- {
- Arguments = args,
- FileName = path,
- CreateNoWindow = true,
- WorkingDirectory = workingDirectory,
- WindowStyle = ProcessWindowStyle.Normal
- }))
- {
- process.WaitForExit();
- }
- }
}
}
diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
index e6696b8c4..76c58d5dc 100644
--- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
+++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO
public string Extension => ".mblink";
- public string Resolve(string shortcutPath)
+ public string? Resolve(string shortcutPath)
{
if (string.IsNullOrEmpty(shortcutPath))
{
diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs
index c16ebd61b..e4f5f4cf0 100644
--- a/Emby.Server.Implementations/IO/StreamHelper.cs
+++ b/Emby.Server.Implementations/IO/StreamHelper.cs
@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.IO
{
public class StreamHelper : IStreamHelper
{
- public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
+ public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index 4bef59543..a430b9e72 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,7 +1,5 @@
#pragma warning disable CS1591
-using System;
-
namespace Emby.Server.Implementations
{
public interface IStartupOptions
@@ -9,7 +7,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the value of the --ffmpeg command line option.
/// </summary>
- string FFmpegPath { get; }
+ string? FFmpegPath { get; }
/// <summary>
/// Gets the value of the --service command line option.
@@ -19,21 +17,21 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the value of the --package-name command line option.
/// </summary>
- string PackageName { get; }
+ string? PackageName { get; }
/// <summary>
/// Gets the value of the --restartpath command line option.
/// </summary>
- string RestartPath { get; }
+ string? RestartPath { get; }
/// <summary>
/// Gets the value of the --restartargs command line option.
/// </summary>
- string RestartArgs { get; }
+ string? RestartArgs { get; }
/// <summary>
/// Gets the value of the --published-server-url command line option.
/// </summary>
- Uri PublishedServerUrl { get; }
+ string? PublishedServerUrl { get; }
}
}
diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
index afa4ec7b1..e96b64595 100644
--- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
@@ -2,20 +2,12 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 5f7e51858..833fb0b7a 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -191,7 +193,7 @@ namespace Emby.Server.Implementations.Images
InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray()
};
- if (options.InputPaths.Length == 0)
+ if (options.InputPaths.Count == 0)
{
return null;
}
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index 161b4c452..ff5f26ce0 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 462eb03a8..900b3fd9c 100644
--- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -1,10 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 0224ab32a..859017f86 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 381788231..6da431c68 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
index 0ce1b91e8..b8f0f0d65 100644
--- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -29,9 +31,7 @@ namespace Emby.Server.Implementations.Images
{
var subItem = i.Item2;
- var episode = subItem as Episode;
-
- if (episode != null)
+ if (subItem is Episode episode)
{
var series = episode.Series;
if (series != null && series.HasImage(ImageType.Primary))
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 3380e29d4..c7d113963 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
- if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
+ if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 236453e80..6c65b5899 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index e30a67593..5384c04b3 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Linq;
using DotNet.Globbing;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index db27862ce..028673529 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -48,6 +50,7 @@ using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
using VideoResolver = Emby.Naming.Video.VideoResolver;
@@ -175,10 +178,7 @@ namespace Emby.Server.Implementations.Library
{
lock (_rootFolderSyncLock)
{
- if (_rootFolder == null)
- {
- _rootFolder = CreateRootFolder();
- }
+ _rootFolder ??= CreateRootFolder();
}
}
@@ -196,33 +196,33 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; }
+ private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
- private IIntroProvider[] IntroProviders { get; set; }
+ private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
- private IItemResolver[] EntityResolvers { get; set; }
+ private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
- private IMultiItemResolver[] MultiItemResolvers { get; set; }
+ private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
/// <summary>
/// Gets or sets the comparers.
/// </summary>
/// <value>The comparers.</value>
- private IBaseItemComparer[] Comparers { get; set; }
+ private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
public bool IsScanRunning { get; private set; }
@@ -558,7 +558,6 @@ namespace Emby.Server.Implementations.Library
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
{
Parent = parent,
- Path = fullPath,
FileInfo = fileInfo,
CollectionType = collectionType,
LibraryOptions = libraryOptions
@@ -684,7 +683,7 @@ namespace Emby.Server.Implementations.Library
foreach (var item in items)
{
- ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService);
+ ResolverHelper.SetInitialItemValues(item, parent, this, directoryService);
}
items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions));
@@ -697,25 +696,32 @@ namespace Emby.Server.Implementations.Library
}
private IEnumerable<BaseItem> ResolveFileList(
- IEnumerable<FileSystemMetadata> fileList,
+ IReadOnlyList<FileSystemMetadata> fileList,
IDirectoryService directoryService,
Folder parent,
string collectionType,
IItemResolver[] resolvers,
LibraryOptions libraryOptions)
{
- return fileList.Select(f =>
+ // Given that fileList is a list we can save enumerator allocations by indexing
+ for (var i = 0; i < fileList.Count; i++)
{
+ var file = fileList[i];
+ BaseItem result = null;
try
{
- return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
+ result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error resolving path {path}", f.FullName);
- return null;
+ _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
+ }
+
+ if (result != null)
+ {
+ yield return result;
}
- }).Where(i => i != null);
+ }
}
/// <summary>
@@ -1066,17 +1072,17 @@ namespace Emby.Server.Implementations.Library
// Start by just validating the children of the root, but go no further
await RootFolder.ValidateChildren(
new SimpleProgress<double>(),
- cancellationToken,
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
- recursive: false).ConfigureAwait(false);
+ recursive: false,
+ cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().ValidateChildren(
new SimpleProgress<double>(),
- cancellationToken,
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
- recursive: false).ConfigureAwait(false);
+ recursive: false,
+ cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
@@ -1096,7 +1102,7 @@ namespace Emby.Server.Implementations.Library
innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
// Validate the entire media library
- await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
+ await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
progress.Report(96);
@@ -1163,7 +1169,7 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100);
}
- _itemRepository.UpdateInheritedValues(cancellationToken);
+ _itemRepository.UpdateInheritedValues();
progress.Report(100);
}
@@ -1240,11 +1246,20 @@ namespace Emby.Server.Implementations.Library
return info;
}
- private string GetCollectionType(string path)
+ private CollectionTypeOptions? GetCollectionType(string path)
{
- return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false)
- .Select(Path.GetFileNameWithoutExtension)
- .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
+ foreach (var file in files)
+ {
+ // TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
+ // https://github.com/dotnet/runtime/issues/20008
+ if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
+ {
+ return res;
+ }
+ }
+
+ return null;
}
/// <summary>
@@ -1905,12 +1920,17 @@ namespace Emby.Server.Implementations.Library
}
catch (ArgumentException)
{
- _logger.LogWarning("Cannot get image index for {0}", img.Path);
+ _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
+ continue;
+ }
+ catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
+ {
+ _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
continue;
}
- catch (InvalidOperationException)
+ catch (HttpRequestException ex)
{
- _logger.LogWarning("Cannot fetch image from {0}", img.Path);
+ _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", img.Path, ex.StatusCode);
continue;
}
}
@@ -1923,7 +1943,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path);
+ _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
image.Width = 0;
image.Height = 0;
continue;
@@ -1935,7 +1955,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
+ _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
image.BlurHash = string.Empty;
}
@@ -1945,7 +1965,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
+ _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
}
}
@@ -2063,7 +2083,7 @@ namespace Emby.Server.Implementations.Library
return new List<Folder>();
}
- return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList());
+ return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>());
}
public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren)
@@ -2088,10 +2108,10 @@ namespace Emby.Server.Implementations.Library
return GetCollectionFoldersInternal(item, allUserRootChildren);
}
- private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren)
+ private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
{
return allUserRootChildren
- .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase))
+ .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
@@ -2099,9 +2119,9 @@ namespace Emby.Server.Implementations.Library
{
if (!(item is CollectionFolder collectionFolder))
{
+ // List.Find is more performant than FirstOrDefault due to enumerator allocation
collectionFolder = GetCollectionFolders(item)
- .OfType<CollectionFolder>()
- .FirstOrDefault();
+ .Find(folder => folder is CollectionFolder) as CollectionFolder;
}
return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
@@ -2487,8 +2507,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public bool IsVideoFile(string path)
{
- var resolver = new VideoResolver(GetNamingOptions());
- return resolver.IsVideoFile(path);
+ return VideoResolver.IsVideoFile(path, GetNamingOptions());
}
/// <inheritdoc />
@@ -2503,7 +2522,7 @@ namespace Emby.Server.Implementations.Library
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
{
var series = episode.Series;
- bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
+ bool? isAbsoluteNaming = series != null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
if (!isAbsoluteNaming.Value)
{
// In other words, no filter applied
@@ -2515,9 +2534,23 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
// TODO nullable - what are we trying to do there with empty episodeInfo?
- var episodeInfo = episode.IsFileProtocol
- ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path)
- : new Naming.TV.EpisodeInfo(episode.Path);
+ EpisodeInfo episodeInfo = null;
+ if (episode.IsFileProtocol)
+ {
+ episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
+ // Resolve from parent folder if it's not the Season folder
+ if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder))
+ {
+ episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming);
+ if (episodeInfo != null)
+ {
+ // add the container
+ episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
+ }
+ }
+ }
+
+ episodeInfo ??= new EpisodeInfo(episode.Path);
try
{
@@ -2652,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
return changed;
}
+ /// <inheritdoc />
public NamingOptions GetNamingOptions()
{
if (_namingOptions == null)
@@ -2665,13 +2699,12 @@ namespace Emby.Server.Implementations.Library
public ItemLookupInfo ParseName(string name)
{
- var resolver = new VideoResolver(GetNamingOptions());
-
- var result = resolver.CleanDateTime(name);
+ var namingOptions = GetNamingOptions();
+ var result = VideoResolver.CleanDateTime(name, namingOptions);
return new ItemLookupInfo
{
- Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
+ Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name,
Year = result.Year
};
}
@@ -2685,9 +2718,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
- var videoListResolver = new VideoListResolver(namingOptions);
-
- var videos = videoListResolver.Resolve(fileSystemChildren);
+ var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
@@ -2731,9 +2762,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
- var videoListResolver = new VideoListResolver(namingOptions);
-
- var videos = videoListResolver.Resolve(fileSystemChildren);
+ var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
@@ -2767,6 +2796,7 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
{
+ string newPath;
if (ownerItem != null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2774,15 +2804,9 @@ namespace Emby.Server.Implementations.Library
{
foreach (var pathInfo in libraryOptions.PathInfos)
{
- if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
+ if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
{
- continue;
- }
-
- var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
- if (substitutionResult.Item2)
- {
- return substitutionResult.Item1;
+ return newPath;
}
}
}
@@ -2791,24 +2815,16 @@ namespace Emby.Server.Implementations.Library
var metadataPath = _configurationManager.Configuration.MetadataPath;
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
- if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
+ if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
{
- var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
- if (metadataSubstitutionResult.Item2)
- {
- return metadataSubstitutionResult.Item1;
- }
+ return newPath;
}
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
{
- if (!string.IsNullOrWhiteSpace(map.From))
+ if (path.TryReplaceSubPath(map.From, map.To, out newPath))
{
- var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
- if (substitutionResult.Item2)
- {
- return substitutionResult.Item1;
- }
+ return newPath;
}
}
@@ -2817,47 +2833,12 @@ namespace Emby.Server.Implementations.Library
public string SubstitutePath(string path, string from, string to)
{
- return SubstitutePathInternal(path, from, to).Item1;
- }
-
- private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
- {
- if (string.IsNullOrWhiteSpace(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- if (string.IsNullOrWhiteSpace(from))
- {
- throw new ArgumentNullException(nameof(from));
- }
-
- if (string.IsNullOrWhiteSpace(to))
+ if (path.TryReplaceSubPath(from, to, out var newPath))
{
- throw new ArgumentNullException(nameof(to));
+ return newPath;
}
- from = from.Trim();
- to = to.Trim();
-
- var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
- var changed = false;
-
- if (!string.Equals(newPath, path, StringComparison.Ordinal))
- {
- if (to.IndexOf('/', StringComparison.Ordinal) != -1)
- {
- newPath = newPath.Replace('\\', '/');
- }
- else
- {
- newPath = newPath.Replace('/', '\\');
- }
-
- changed = true;
- }
-
- return new Tuple<string, bool>(newPath, changed);
+ return path;
}
private void SetExtraTypeFromFilename(Video item)
@@ -2915,12 +2896,20 @@ namespace Emby.Server.Implementations.Library
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
{
+ UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ /// <inheritdoc />
+ public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
+ {
if (!item.SupportsPeople)
{
return;
}
_itemRepository.UpdatePeople(item.Id, people);
+
+ await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
@@ -2956,7 +2945,7 @@ namespace Emby.Server.Implementations.Library
throw new InvalidOperationException();
}
- public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
+ public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
{
@@ -2990,9 +2979,9 @@ namespace Emby.Server.Implementations.Library
{
Directory.CreateDirectory(virtualFolderPath);
- if (!string.IsNullOrEmpty(collectionType))
+ if (collectionType != null)
{
- var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
+ var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>());
}
@@ -3024,6 +3013,58 @@ namespace Emby.Server.Implementations.Library
}
}
+ private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
+ {
+ var personsToSave = new List<BaseItem>();
+
+ foreach (var person in people)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var itemUpdateType = ItemUpdateType.MetadataDownload;
+ var saveEntity = false;
+ var personEntity = GetPerson(person.Name);
+
+ // if PresentationUniqueKey is empty it's likely a new item.
+ if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+ {
+ personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
+ saveEntity = true;
+ }
+
+ foreach (var id in person.ProviderIds)
+ {
+ if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
+ {
+ personEntity.SetProviderId(id.Key, id.Value);
+ saveEntity = true;
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
+ {
+ personEntity.SetImage(
+ new ItemImageInfo
+ {
+ Path = person.ImageUrl,
+ Type = ImageType.Primary
+ },
+ 0);
+
+ saveEntity = true;
+ itemUpdateType = ItemUpdateType.ImageUpdate;
+ }
+
+ if (saveEntity)
+ {
+ personsToSave.Add(personEntity);
+ await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+ }
+ }
+
+ CreateItems(personsToSave, null, CancellationToken.None);
+ }
+
private void StartScanInBackground()
{
Task.Run(() =>
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 2070df31e..4ef7923db 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -25,7 +27,7 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
{
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index c63eb7017..b812b6b61 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -46,7 +48,7 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private IMediaSourceProvider[] _providers;
@@ -199,10 +201,15 @@ namespace Emby.Server.Implementations.Library
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
+ else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ {
+ source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
+ source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
+ }
}
}
- return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
+ return SortMediaSources(list);
}
public MediaProtocol GetPathProtocol(string path)
@@ -345,7 +352,7 @@ namespace Emby.Server.Implementations.Library
private string[] NormalizeLanguage(string language)
{
- if (language == null)
+ if (string.IsNullOrEmpty(language))
{
return Array.Empty<string>();
}
@@ -374,8 +381,7 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
- ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
+ var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex == null
@@ -404,9 +410,7 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
- ? Array.Empty<string>()
- : NormalizeLanguage(user.AudioLanguagePreference);
+ var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
@@ -436,7 +440,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
+ private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@@ -451,8 +455,9 @@ namespace Emby.Server.Implementations.Library
{
var stream = i.VideoStream;
- return stream == null || stream.Width == null ? 0 : stream.Width.Value;
+ return stream?.Width ?? 0;
})
+ .Where(i => i.Type != MediaSourceType.Placeholder)
.ToList();
}
@@ -584,18 +589,9 @@ namespace Emby.Server.Implementations.Library
public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
- var info = _openStreams.Values.FirstOrDefault(i =>
- {
- var liveStream = i as ILiveStream;
- if (liveStream != null)
- {
- return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
- }
-
- return false;
- });
+ var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
- return Task.FromResult(info as IDirectStreamProvider);
+ return Task.FromResult(info.Value as IDirectStreamProvider);
}
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 28fa06239..b833122ea 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 658c53f28..06300adeb 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -100,8 +102,7 @@ namespace Emby.Server.Implementations.Library
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
{
- var genre = item as MusicGenre;
- if (genre != null)
+ if (item is MusicGenre genre)
{
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
}
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 06ff3e611..86b8039fa 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -1,7 +1,6 @@
-#nullable enable
-
using System;
-using System.Text.RegularExpressions;
+using System.Diagnostics.CodeAnalysis;
+using MediaBrowser.Common.Providers;
namespace Emby.Server.Implementations.Library
{
@@ -41,11 +40,78 @@ namespace Emby.Server.Implementations.Library
// for imdbid we also accept pattern matching
if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
{
- var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
- return m.Success ? m.Value : null;
+ var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId);
+ return match ? imdbId.ToString() : null;
}
return null;
}
+
+ /// <summary>
+ /// Replaces a sub path with another sub path and normalizes the final path.
+ /// </summary>
+ /// <param name="path">The original path.</param>
+ /// <param name="subPath">The original sub path.</param>
+ /// <param name="newSubPath">The new sub path.</param>
+ /// <param name="newPath">The result of the sub path replacement</param>
+ /// <returns>The path after replacing the sub path.</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
+ public static bool TryReplaceSubPath(
+ [NotNullWhen(true)] this string? path,
+ [NotNullWhen(true)] string? subPath,
+ [NotNullWhen(true)] string? newSubPath,
+ [NotNullWhen(true)] out string? newPath)
+ {
+ newPath = null;
+
+ if (string.IsNullOrEmpty(path)
+ || string.IsNullOrEmpty(subPath)
+ || string.IsNullOrEmpty(newSubPath)
+ || subPath.Length > path.Length)
+ {
+ return false;
+ }
+
+ char oldDirectorySeparatorChar;
+ char newDirectorySeparatorChar;
+ // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
+ // The reasoning behind this is that a forward slash likely means it's a Linux path and
+ // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
+ if (newSubPath.Contains('/', StringComparison.Ordinal))
+ {
+ oldDirectorySeparatorChar = '\\';
+ newDirectorySeparatorChar = '/';
+ }
+ else
+ {
+ oldDirectorySeparatorChar = '/';
+ newDirectorySeparatorChar = '\\';
+ }
+
+ path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
+ subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
+
+ // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
+ // when the sub path matches a similar but in-complete subpath
+ var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
+ if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (path.Length > subPath.Length
+ && !oldSubPathEndsWithSeparator
+ && path[subPath.Length] != newDirectorySeparatorChar)
+ {
+ return false;
+ }
+
+ var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar);
+ // Ensure that the path with the old subpath removed starts with a leading dir separator
+ int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length;
+ newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx));
+
+ return true;
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index 4e4cac75b..ac75e5d3a 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -18,11 +18,10 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="item">The item.</param>
/// <param name="parent">The parent.</param>
- /// <param name="fileSystem">The file system.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="directoryService">The directory service.</param>
- /// <exception cref="ArgumentException">Item must have a path</exception>
- public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService)
+ /// <exception cref="ArgumentException">Item must have a path.</exception>
+ public static void SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService)
{
// This version of the below method has no ItemResolveArgs, so we have to require the path already being set
if (string.IsNullOrEmpty(item.Path))
@@ -43,9 +42,14 @@ namespace Emby.Server.Implementations.Library
// Make sure DateCreated and DateModified have values
var fileInfo = directoryService.GetFile(item.Path);
- SetDateCreated(item, fileSystem, fileInfo);
+ if (fileInfo == null)
+ {
+ throw new FileNotFoundException("Can't find item path.", item.Path);
+ }
- EnsureName(item, item.Path, fileInfo);
+ SetDateCreated(item, fileInfo);
+
+ EnsureName(item, fileInfo);
}
/// <summary>
@@ -72,9 +76,9 @@ namespace Emby.Server.Implementations.Library
item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
// Make sure the item has a name
- EnsureName(item, item.Path, args.FileInfo);
+ EnsureName(item, args.FileInfo);
- item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) ||
item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values
@@ -84,29 +88,16 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Ensures the name.
/// </summary>
- private static void EnsureName(BaseItem item, string fullPath, FileSystemMetadata fileInfo)
+ private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo)
{
// If the subclass didn't supply a name, add it here
- if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(fullPath))
+ if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
{
- var fileName = fileInfo == null ? Path.GetFileName(fullPath) : fileInfo.Name;
-
- item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory);
+ item.Name = fileInfo.IsDirectory ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name);
}
}
/// <summary>
- /// Gets the display name.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="isDirectory">if set to <c>true</c> [is directory].</param>
- /// <returns>System.String.</returns>
- private static string GetDisplayName(string path, bool isDirectory)
- {
- return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
- }
-
- /// <summary>
/// Ensures DateCreated and DateModified have values.
/// </summary>
/// <param name="fileSystem">The file system.</param>
@@ -114,21 +105,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="args">The args.</param>
private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args)
{
- if (fileSystem == null)
- {
- throw new ArgumentNullException(nameof(fileSystem));
- }
-
- if (item == null)
- {
- throw new ArgumentNullException(nameof(item));
- }
-
- if (args == null)
- {
- throw new ArgumentNullException(nameof(args));
- }
-
// See if a different path came out of the resolver than what went in
if (!fileSystem.AreEqual(args.Path, item.Path))
{
@@ -136,7 +112,7 @@ namespace Emby.Server.Implementations.Library
if (childData != null)
{
- SetDateCreated(item, fileSystem, childData);
+ SetDateCreated(item, childData);
}
else
{
@@ -144,17 +120,17 @@ namespace Emby.Server.Implementations.Library
if (fileData.Exists)
{
- SetDateCreated(item, fileSystem, fileData);
+ SetDateCreated(item, fileData);
}
}
}
else
{
- SetDateCreated(item, fileSystem, args.FileInfo);
+ SetDateCreated(item, args.FileInfo);
}
}
- private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info)
+ private static void SetDateCreated(BaseItem item, FileSystemMetadata? info)
{
var config = BaseItem.ConfigurationManager.GetMetadataConfiguration();
@@ -163,7 +139,7 @@ namespace Emby.Server.Implementations.Library
// directoryService.getFile may return null
if (info != null)
{
- var dateCreated = fileSystem.GetCreationTimeUtc(info);
+ var dateCreated = info.CreationTimeUtc;
if (dateCreated.Equals(DateTime.MinValue))
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 90b6a8a7d..e893d6335 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -201,6 +203,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue;
}
+ if (resolvedItem.Files.Count == 0)
+ {
+ continue;
+ }
+
var firstMedia = resolvedItem.Files[0];
var libraryItem = new T
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index bf32381eb..8e1eccb10 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 60f82806f..3d2ae95d2 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Linq;
using System.Threading.Tasks;
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 2f5e46038..cdb492022 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -30,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- protected override T Resolve(ItemResolveArgs args)
+ public override T Resolve(ItemResolveArgs args)
{
return ResolveVideo<T>(args, false);
}
@@ -42,14 +44,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <param name="args">The args.</param>
/// <param name="parseName">if set to <c>true</c> [parse name].</param>
/// <returns>``0.</returns>
- protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
+ protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+ var namingOptions = LibraryManager.GetNamingOptions();
// If the path is a file check for a matching extensions
- var parser = new VideoResolver(namingOptions);
-
if (args.IsDirectory)
{
TVideoType video = null;
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else if (IsDvdFile(filename))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else
{
- var videoInfo = parser.Resolve(args.Path, false, false);
+ var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
if (videoInfo == null)
{
@@ -165,13 +165,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
{
- var extension = Path.GetExtension(video.Path);
- video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
- VideoType.Iso :
- VideoType.VideoFile;
+ var extension = Path.GetExtension(video.Path.AsSpan());
+ video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".img", StringComparison.OrdinalIgnoreCase)
+ ? VideoType.Iso
+ : VideoType.VideoFile;
- video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+ video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
video.IsPlaceHolder = videoInfo.IsStub;
if (videoInfo.IsStub)
@@ -193,11 +193,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (video.VideoType == VideoType.Iso)
{
- if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1)
+ if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase))
{
video.IsoType = IsoType.Dvd;
}
- else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1)
+ else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase))
{
video.IsoType = IsoType.BluRay;
}
@@ -250,10 +250,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void Set3DFormat(Video video)
{
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
-
- var resolver = new Format3DParser(namingOptions);
- var result = resolver.Parse(video.Path);
+ var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
Set3DFormat(video, result.Is3D, result.Format3D);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 86242d137..68076730b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -13,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
- protected override Book Resolve(ItemResolveArgs args)
+ public override Book Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
index 7dbce7a6e..7aaee017d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
index 9ca76095b..fa45ccf84 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@@ -12,6 +14,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
where T : BaseItem, new()
{
/// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public virtual ResolverPriority Priority => ResolverPriority.First;
+
+ /// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
@@ -22,12 +30,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
/// <summary>
- /// Gets the priority.
- /// </summary>
- /// <value>The priority.</value>
- public virtual ResolverPriority Priority => ResolverPriority.First;
-
- /// <summary>
/// Sets initial values on the newly resolved item.
/// </summary>
/// <param name="item">The item.</param>
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 295e9e120..69d71d0d9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.IO;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 8ef7172de..97f96f746 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -1,9 +1,12 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -69,6 +72,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result;
}
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Video.</returns>
+ public override Video Resolve(ItemResolveArgs args)
+ {
+ var collectionType = args.GetCollectionType();
+
+ // Find movies with their own folders
+ if (args.IsDirectory)
+ {
+ if (IsInvalid(args.Parent, collectionType))
+ {
+ return null;
+ }
+
+ var files = args.FileSystemChildren
+ .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
+ .ToList();
+
+ if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ }
+
+ if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ }
+
+ if (string.IsNullOrEmpty(collectionType))
+ {
+ // Owned items will be caught by the plain video resolver
+ if (args.Parent == null)
+ {
+ // return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
+ return null;
+ }
+
+ if (args.HasParent<Series>())
+ {
+ return null;
+ }
+
+ {
+ return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ }
+ }
+
+ if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ }
+
+ return null;
+ }
+
+ // Handle owned items
+ if (args.Parent == null)
+ {
+ return base.Resolve(args);
+ }
+
+ if (IsInvalid(args.Parent, collectionType))
+ {
+ return null;
+ }
+
+ Video item = null;
+
+ if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ 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))
+ {
+ item = ResolveVideo<Movie>(args, true);
+ }
+ else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ {
+ item = ResolveVideo<Video>(args, false);
+ }
+ else if (string.IsNullOrEmpty(collectionType))
+ {
+ if (args.HasParent<Series>())
+ {
+ return null;
+ }
+
+ item = ResolveVideo<Video>(args, false);
+ }
+
+ if (item != null)
+ {
+ item.IsInMixedFolder = true;
+ }
+
+ return item;
+ }
+
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
@@ -151,10 +258,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+ var namingOptions = LibraryManager.GetNamingOptions();
- var resolver = new VideoListResolver(namingOptions);
- var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList();
+ var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
var result = new MultiItemResolverResult
{
@@ -217,110 +323,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
/// <summary>
- /// Resolves the specified args.
- /// </summary>
- /// <param name="args">The args.</param>
- /// <returns>Video.</returns>
- protected override Video Resolve(ItemResolveArgs args)
- {
- var collectionType = args.GetCollectionType();
-
- // Find movies with their own folders
- if (args.IsDirectory)
- {
- if (IsInvalid(args.Parent, collectionType))
- {
- return null;
- }
-
- var files = args.FileSystemChildren
- .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
- .ToList();
-
- if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
- {
- return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
- }
-
- if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
- {
- return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
- }
-
- if (string.IsNullOrEmpty(collectionType))
- {
- // Owned items will be caught by the plain video resolver
- if (args.Parent == null)
- {
- // return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
- return null;
- }
-
- if (args.HasParent<Series>())
- {
- return null;
- }
-
- {
- return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
- }
- }
-
- if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
- {
- return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
- }
-
- return null;
- }
-
- // Handle owned items
- if (args.Parent == null)
- {
- return base.Resolve(args);
- }
-
- if (IsInvalid(args.Parent, collectionType))
- {
- return null;
- }
-
- Video item = null;
-
- if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
- {
- 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))
- {
- item = ResolveVideo<Movie>(args, true);
- }
- else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
- {
- item = ResolveVideo<Video>(args, false);
- }
- else if (string.IsNullOrEmpty(collectionType))
- {
- if (args.HasParent<Series>())
- {
- return null;
- }
-
- item = ResolveVideo<Video>(args, false);
- }
-
- if (item != null)
- {
- item.IsInMixedFolder = true;
- }
-
- return item;
- }
-
- /// <summary>
/// Sets the initial item values.
/// </summary>
/// <param name="item">The item.</param>
@@ -376,7 +378,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
var multiDiscFolders = new List<FileSystemMetadata>();
- var libraryOptions = args.GetLibraryOptions();
+ var libraryOptions = args.LibraryOptions;
var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos;
var photos = new List<FileSystemMetadata>();
@@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return returnVideo;
}
- private bool IsInvalid(Folder parent, string collectionType)
+ private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
{
if (parent != null)
{
@@ -545,12 +547,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- if (string.IsNullOrEmpty(collectionType))
+ if (collectionType.IsEmpty)
{
return false;
}
- return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
+ return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
index 3ac837057..534bc80dd 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -13,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
{
private readonly IImageProcessor _imageProcessor;
- private ILibraryManager _libraryManager;
+ private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
@@ -26,6 +28,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_libraryManager = libraryManager;
}
+ /// <inheritdoc />
+ public override ResolverPriority Priority => ResolverPriority.Second;
+
/// <summary>
/// Resolves the specified args.
/// </summary>
@@ -39,8 +44,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.GetLibraryOptions().EnablePhotos))
+ if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
+ || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
{
if (HasPhotos(args))
{
@@ -84,8 +89,5 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false;
}
-
- /// <inheritdoc />
- public override ResolverPriority Priority => ResolverPriority.Second;
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index bcfcee9c6..57bf40e9e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
var collectionType = args.CollectionType;
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
- || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
+ || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && 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 c76d41e5c..ecd44be47 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
Path = args.Path,
Name = Path.GetFileNameWithoutExtension(args.Path),
- IsInMixedFolder = true
+ IsInMixedFolder = true,
+ PlaylistMediaType = MediaType.Audio
};
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
index 99f304190..7b4e14334 100644
--- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 2f7af60c0..d6ae91056 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -1,5 +1,8 @@
+#nullable disable
+
using System;
using System.Linq;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -12,11 +15,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
public class EpisodeResolver : BaseVideoResolver<Episode>
{
/// <summary>
+ /// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public EpisodeResolver(ILibraryManager libraryManager)
+ : base(libraryManager)
+ {
+ }
+
+ /// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Episode.</returns>
- protected override Episode Resolve(ItemResolveArgs args)
+ public override Episode Resolve(ItemResolveArgs args)
{
var parent = args.Parent;
@@ -25,30 +37,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
- var season = parent as Season;
-
// Just in case the user decided to nest episodes.
// Not officially supported but in some cases we can handle it.
- if (season == null)
- {
- season = parent.GetParents().OfType<Season>().FirstOrDefault();
- }
- // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
+ var season = parent as Season ?? parent.GetParents().OfType<Season>().FirstOrDefault();
+
+ // 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 != null ||
- string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
- args.HasParent<Series>())
+ if ((season != null ||
+ string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
+ args.HasParent<Series>())
+ && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
{
var episode = ResolveVideo<Episode>(args, false);
if (episode != null)
{
- var series = parent as Series;
- if (series == null)
- {
- series = parent.GetParents().OfType<Series>().FirstOrDefault();
- }
+ var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
if (series != null)
{
@@ -74,14 +79,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
- /// </summary>
- /// <param name="libraryManager">The library manager.</param>
- public EpisodeResolver(ILibraryManager libraryManager)
- : base(libraryManager)
- {
- }
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 3332e1806..7d707df18 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Globalization;
using Emby.Naming.TV;
using MediaBrowser.Controller.Entities.TV;
@@ -88,7 +90,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"),
seasonNumber,
- args.GetLibraryOptions().PreferredMetadataLanguage);
+ args.LibraryOptions.PreferredMetadataLanguage);
}
return season;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index 732bfd94d..a1562abd3 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -19,19 +21,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
public class SeriesResolver : FolderResolver<Series>
{
- private readonly IFileSystem _fileSystem;
private readonly ILogger<SeriesResolver> _logger;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="SeriesResolver"/> class.
/// </summary>
- /// <param name="fileSystem">The file system.</param>
/// <param name="logger">The logger.</param>
/// <param name="libraryManager">The library manager.</param>
- public SeriesResolver(IFileSystem fileSystem, ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
+ public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
{
- _fileSystem = fileSystem;
_logger = logger;
_libraryManager = libraryManager;
}
@@ -59,15 +58,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
- // if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
- //{
- // return new Series
- // {
- // Path = args.Path,
- // Name = Path.GetFileName(args.Path)
- // };
- //}
-
var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
@@ -100,7 +90,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
- if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, false))
+ if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false))
{
return new Series
{
@@ -117,8 +107,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
public static bool IsSeriesFolder(
string path,
IEnumerable<FileSystemMetadata> fileSystemChildren,
- IDirectoryService directoryService,
- IFileSystem fileSystem,
ILogger<SeriesResolver> logger,
ILibraryManager libraryManager,
bool isTvContentType)
@@ -127,7 +115,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
- if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
+ if (IsSeasonFolder(child.FullName, isTvContentType))
{
logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@@ -161,31 +149,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
}
/// <summary>
- /// Determines whether [is place holder] [the specified path].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns><c>true</c> if [is place holder] [the specified path]; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">path</exception>
- private static bool IsVideoPlaceHolder(string path)
- {
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- var extension = Path.GetExtension(path);
-
- return string.Equals(extension, ".disc", StringComparison.OrdinalIgnoreCase);
- }
-
- /// <summary>
/// Determines whether [is season folder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
- /// <param name="libraryManager">The library manager.</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
- private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager)
+ private static bool IsSeasonFolder(string path, bool isTvContentType)
{
var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
index 62268fce9..9599faea4 100644
--- a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 94602582b..26e615fa0 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -12,7 +14,6 @@ using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
-using Microsoft.Extensions.Logging;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index d16275b19..8aa605a90 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -13,8 +15,8 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using Book = MediaBrowser.Controller.Entities.Book;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
+using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library
{
@@ -220,7 +222,7 @@ namespace Emby.Server.Implementations.Library
var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration
- if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
+ if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
{
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
@@ -239,7 +241,7 @@ namespace Emby.Server.Implementations.Library
{
// Enforce MinResumeDuration
var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
- if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book))
+ if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
{
positionTicks = 0;
data.Played = playedToCompletion = true;
@@ -248,15 +250,15 @@ namespace Emby.Server.Implementations.Library
}
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
{
- var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
- var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
+ var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
+ var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
- if (minIn > _config.Configuration.MinAudiobookResume)
+ if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
{
// ignore progress during the beginning
positionTicks = 0;
}
- else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
+ else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
{
// mark as completed close to the end
positionTicks = 0;
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index b6b7ea949..e2da672a3 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -1,8 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 341194f23..3fcadf5b1 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -45,7 +47,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
- using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
onStarted();
@@ -70,7 +73,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
- await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
onStarted();
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 2c0de661d..797063120 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -17,7 +19,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -802,22 +803,22 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public ActiveRecordingInfo GetActiveRecordingInfo(string path)
{
- if (string.IsNullOrWhiteSpace(path))
+ if (string.IsNullOrWhiteSpace(path) || _activeRecordings.IsEmpty)
{
return null;
}
- foreach (var recording in _activeRecordings.Values)
+ foreach (var (_, recordingInfo) in _activeRecordings)
{
- if (string.Equals(recording.Path, path, StringComparison.Ordinal) && !recording.CancellationTokenSource.IsCancellationRequested)
+ if (string.Equals(recordingInfo.Path, path, StringComparison.Ordinal) && !recordingInfo.CancellationTokenSource.IsCancellationRequested)
{
- var timer = recording.Timer;
+ var timer = recordingInfo.Timer;
if (timer.Status != RecordingStatus.InProgress)
{
return null;
}
- return recording;
+ return recordingInfo;
}
}
@@ -1622,9 +1623,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
return _activeRecordings
- .Values
- .ToList()
- .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
+ .Any(i => string.Equals(i.Value.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Value.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
}
private IRecorder GetRecorder(MediaSourceInfo mediaSource)
@@ -1856,7 +1855,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -1920,7 +1920,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -2238,14 +2239,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var enabledTimersForSeries = new List<TimerInfo>();
foreach (var timer in allTimers)
{
- var existingTimer = _timerProvider.GetTimer(timer.Id);
-
- if (existingTimer == null)
- {
- existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId)
+ var existingTimer = _timerProvider.GetTimer(timer.Id)
+ ?? (string.IsNullOrWhiteSpace(timer.ProgramId)
? null
- : _timerProvider.GetTimerByProgramId(timer.ProgramId);
- }
+ : _timerProvider.GetTimerByProgramId(timer.ProgramId));
if (existingTimer == null)
{
@@ -2604,7 +2601,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Movies",
- CollectionType = CollectionType.Movies
+ CollectionType = CollectionTypeOptions.Movies
};
}
@@ -2615,7 +2612,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Shows",
- CollectionType = CollectionType.TvShows
+ CollectionType = CollectionTypeOptions.TvShows
};
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 78a82118e..26e4ef1ed 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,6 +12,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -28,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IServerApplicationPaths _appPaths;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
@@ -307,13 +310,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
using (var reader = new StreamReader(source))
{
- while (!reader.EndOfStream)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
- await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ await target.WriteAsync(bytes.AsMemory()).ConfigureAwait(false);
await target.FlushAsync().ConfigureAwait(false);
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
index 463d0ed0a..0ec52a959 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
@@ -6,58 +6,46 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
-
internal class EpgChannelData
{
+
+ private readonly Dictionary<string, ChannelInfo> _channelsById;
+
+ private readonly Dictionary<string, ChannelInfo> _channelsByNumber;
+
+ private readonly Dictionary<string, ChannelInfo> _channelsByName;
+
public EpgChannelData(IEnumerable<ChannelInfo> channels)
{
- ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
- ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
- ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ _channelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ _channelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
+ _channelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var channel in channels)
{
- ChannelsById[channel.Id] = channel;
+ _channelsById[channel.Id] = channel;
if (!string.IsNullOrEmpty(channel.Number))
{
- ChannelsByNumber[channel.Number] = channel;
+ _channelsByNumber[channel.Number] = channel;
}
var normalizedName = NormalizeName(channel.Name ?? string.Empty);
if (!string.IsNullOrWhiteSpace(normalizedName))
{
- ChannelsByName[normalizedName] = channel;
+ _channelsByName[normalizedName] = channel;
}
}
}
- private Dictionary<string, ChannelInfo> ChannelsById { get; set; }
+ public ChannelInfo? GetChannelById(string id)
+ => _channelsById.GetValueOrDefault(id);
- private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; }
+ public ChannelInfo? GetChannelByNumber(string number)
+ => _channelsByNumber.GetValueOrDefault(number);
- private Dictionary<string, ChannelInfo> ChannelsByName { get; set; }
-
- public ChannelInfo GetChannelById(string id)
- {
- ChannelsById.TryGetValue(id, out var result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByNumber(string number)
- {
- ChannelsByNumber.TryGetValue(number, out var result);
-
- return result;
- }
-
- public ChannelInfo GetChannelByName(string name)
- {
- ChannelsByName.TryGetValue(name, out var result);
-
- return result;
- }
+ public ChannelInfo? GetChannelByName(string name)
+ => _channelsByName.GetValueOrDefault(name);
public static string NormalizeName(string value)
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index 57424f043..bdab8c3e4 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -1,12 +1,12 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text;
using System.Text.Json;
-using System.Threading.Tasks;
using MediaBrowser.Common.Json;
using Microsoft.Extensions.Logging;
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private T[] _items;
public ItemDataProvider(
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
index 142c59542..32245f899 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
- internal class RecordingHelper
+ internal static class RecordingHelper
{
public static DateTime GetStartTime(TimerInfo timer)
{
@@ -70,17 +70,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private static string GetDateString(DateTime date)
{
- date = date.ToLocalTime();
-
- return string.Format(
- CultureInfo.InvariantCulture,
- "{0}_{1}_{2}_{3}_{4}_{5}",
- date.Year.ToString("0000", CultureInfo.InvariantCulture),
- date.Month.ToString("00", CultureInfo.InvariantCulture),
- date.Day.ToString("00", CultureInfo.InvariantCulture),
- date.Hour.ToString("00", CultureInfo.InvariantCulture),
- date.Minute.ToString("00", CultureInfo.InvariantCulture),
- date.Second.ToString("00", CultureInfo.InvariantCulture));
+ return date.ToLocalTime().ToString("yyyy_MM_dd_HH_mm_ss", CultureInfo.InvariantCulture);
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
index da707fec6..b1259de23 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
@@ -2,7 +2,6 @@
using System;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 1efa90e25..6c52a9a73 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 7567ea312..00d02873c 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -35,8 +37,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
@@ -111,7 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions).ConfigureAwait(false);
+ var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
@@ -122,12 +124,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions).ConfigureAwait(false);
+ var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
- var programIdsWithImages =
- programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
- .ToList();
+ var programIdsWithImages = programDetails
+ .Where(p => p.hasImageArtwork).Select(p => p.programID)
+ .ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
@@ -182,8 +184,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static int GetSizeOrder(ScheduleDirect.ImageData image)
{
- if (!string.IsNullOrWhiteSpace(image.height)
- && int.TryParse(image.height, out int value))
+ if (int.TryParse(image.height, out int value))
{
return value;
}
@@ -704,7 +705,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
- var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
@@ -776,7 +777,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel");
@@ -788,14 +789,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var channelNumber = GetChannelNumber(channel);
- var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase));
- if (station == null)
- {
- station = new ScheduleDirect.Station
+ var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase))
+ ?? new ScheduleDirect.Station
{
stationID = channel.stationID
};
- }
var channelInfo = new ChannelInfo
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 76c875737..ebad4eddf 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -1,10 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Threading;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
index ba916af38..098f193fb 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
@@ -1,21 +1,23 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.LiveTv;
namespace Emby.Server.Implementations.LiveTv
{
+ /// <summary>
+ /// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />.
+ /// </summary>
public class LiveTvConfigurationFactory : IConfigurationFactory
{
+ /// <inheritdoc />
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
- ConfigurationType = typeof(LiveTvOptions),
- Key = "livetv"
+ ConfigurationType = typeof(LiveTvOptions),
+ Key = "livetv"
}
};
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 6af49dd45..21e1409ac 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 63a3146aa..d964769b5 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -987,10 +989,7 @@ namespace Emby.Server.Implementations.LiveTv
var externalProgramId = programTuple.Item2;
string externalSeriesId = programTuple.Item3;
- if (timerList == null)
- {
- timerList = (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
- }
+ timerList ??= (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase));
var foundSeriesTimer = false;
@@ -1018,10 +1017,7 @@ namespace Emby.Server.Implementations.LiveTv
continue;
}
- if (seriesTimerList == null)
- {
- seriesTimerList = (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
- }
+ seriesTimerList ??= (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase));
@@ -1974,10 +1970,7 @@ namespace Emby.Server.Implementations.LiveTv
};
}
- if (service == null)
- {
- service = _services[0];
- }
+ service ??= _services[0];
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
@@ -2273,7 +2266,7 @@ namespace Emby.Server.Implementations.LiveTv
if (dataSourceChanged)
{
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
return info;
@@ -2316,7 +2309,7 @@ namespace Emby.Server.Implementations.LiveTv
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return info;
}
@@ -2328,7 +2321,7 @@ namespace Emby.Server.Implementations.LiveTv
config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
@@ -2362,7 +2355,7 @@ namespace Emby.Server.Implementations.LiveTv
var tunerChannelMappings =
tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index 3a738fd5d..ecd28097d 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
index 582b64923..15df0dcf1 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.LiveTv;
@@ -10,34 +9,55 @@ using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.LiveTv
{
- public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
+ /// <summary>
+ /// The "Refresh Guide" scheduled task.
+ /// </summary>
+ public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILiveTvManager _liveTvManager;
private readonly IConfigurationManager _config;
- public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class.
+ /// </summary>
+ /// <param name="liveTvManager">The live tv manager.</param>
+ /// <param name="config">The configuration manager.</param>
+ public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
{
_liveTvManager = liveTvManager;
_config = config;
}
+ /// <inheritdoc />
public string Name => "Refresh Guide";
+ /// <inheritdoc />
public string Description => "Downloads channel information from live tv services.";
+ /// <inheritdoc />
public string Category => "Live TV";
- public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <inheritdoc />
+ public string Key => "RefreshGuide";
+
+ /// <inheritdoc />
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
var manager = (LiveTvManager)_liveTvManager;
return manager.RefreshChannels(progress, cancellationToken);
}
- /// <summary>
- /// Creates the triggers that define when the task will run.
- /// </summary>
- /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ /// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
@@ -51,13 +71,5 @@ namespace Emby.Server.Implementations.LiveTv
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
-
- public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
-
- public string Key => "RefreshGuide";
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index fbcd4ef37..5941613cf 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -38,6 +40,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public virtual bool IsSupported => true;
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+
public abstract string Type { get; }
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
index 740cbb66e..0f0453189 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
internal class Channels
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
index 09d77f838..42068cd34 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 5ef83f274..54de841fe 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,10 +10,8 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
@@ -19,7 +19,6 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -61,7 +60,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_networkManager = networkManager;
_streamHelper = streamHelper;
- _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions = JsonDefaults.Options;
}
public string Name => "HD Homerun";
@@ -77,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
@@ -185,16 +184,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List<LiveTvTunerInfo>();
- while (!sr.EndOfStream)
+ await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{
- string line = StripXML(sr.ReadLine());
- if (line.Contains("Channel", StringComparison.Ordinal))
+ string stripedLine = StripXML(line);
+ if (stripedLine.Contains("Channel", StringComparison.Ordinal))
{
LiveTvTunerStatus status;
- var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
- var name = line.Substring(0, index - 1);
- var currentChannel = line.Substring(index + 7);
- if (currentChannel != "none")
+ var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = stripedLine.Substring(0, index - 1);
+ var currentChannel = stripedLine.Substring(index + 7);
+ if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
{
status = LiveTvTunerStatus.LiveTv;
}
@@ -335,11 +334,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new Uri(url).AbsoluteUri.TrimEnd('/');
}
- protected EncodingOptions GetEncodingOptions()
- {
- return Config.GetConfiguration<EncodingOptions>("encoding");
- }
-
private static string GetHdHrIdFromChannelId(string channelId)
{
return channelId.Split('_')[1];
@@ -429,10 +423,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
string audioCodec = channelInfo.AudioCodec;
- if (!videoBitrate.HasValue)
- {
- videoBitrate = isHd ? 15000000 : 2000000;
- }
+ videoBitrate ??= isHd ? 15000000 : 2000000;
int? audioBitrate = isHd ? 448000 : 192000;
@@ -592,7 +583,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
- _networkManager,
_streamHelper);
}
@@ -633,7 +623,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
- _networkManager,
_streamHelper);
}
@@ -669,7 +658,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_modelCache.Clear();
}
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
+ using var timedCancellationToken = new CancellationTokenSource(discoveryDurationMs);
+ using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timedCancellationToken.Token, cancellationToken);
+ cancellationToken = linkedCancellationTokenSource.Token;
var list = new List<TunerHostInfo>();
// Create udp broadcast discovery message
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index f09338330..3016eeda2 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -1,7 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Buffers;
+using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
@@ -10,6 +13,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common;
using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -120,17 +124,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
{
- var lockkeyMsg = CreateGetMessage(tuner, "lockkey");
- await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
-
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
- int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
+ await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
- ParseReturnMessage(buffer, receivedBytes, out string returnVal);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
- return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase);
+ return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
}
finally
{
@@ -166,24 +168,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_activeTuner = i;
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
- var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
- await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
- int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
+ await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out _))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
continue;
}
foreach (var command in commands.GetCommands())
{
- var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
- await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
- receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue);
+ await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+ receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out _))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
@@ -191,13 +193,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
- var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
+ var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
- await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
- receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
+ receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out _))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
@@ -232,12 +234,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
foreach (var command in commandList)
{
- var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
- await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
- int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+ var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey);
+ await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out _))
+ if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
return;
}
@@ -265,17 +267,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var stream = client.GetStream();
- var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue);
- await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false);
-
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
- await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
- var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue);
+ var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
+ await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
+
+ await stream.ReadAsync(buffer).ConfigureAwait(false);
+ var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
_lockkey = null;
- await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false);
- await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+ await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
+ await stream.ReadAsync(buffer).ConfigureAwait(false);
}
finally
{
@@ -283,249 +285,136 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- private static byte[] CreateGetMessage(int tuner, string name)
+ internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
{
- var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
- int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length
-
- var message = new byte[messageLength];
+ var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+ int offset = WriteHeaderAndPayload(buffer, byteName);
+ return FinishPacket(buffer, offset);
+ }
- int offset = InsertHeaderAndName(byteName, messageLength, message);
+ internal static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
+ {
+ var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+ int offset = WriteHeaderAndPayload(buffer, byteName);
- bool flipEndian = BitConverter.IsLittleEndian;
+ buffer[offset++] = GetSetValue;
+ offset += WriteNullTerminatedString(buffer.Slice(offset), value);
- // calculate crc and insert at the end of the message
- var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
- if (flipEndian)
+ if (lockkey.HasValue)
{
- Array.Reverse(crcBytes);
+ buffer[offset++] = GetSetLockkey;
+ buffer[offset++] = 4;
+ BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
+ offset += 4;
}
- Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
-
- return message;
+ return FinishPacket(buffer, offset);
}
- private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey)
+ internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload)
{
- var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
- var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value));
-
- int messageLength = byteName.Length + byteValue.Length + 12;
- if (lockkey.HasValue)
- {
- messageLength += 6;
- }
+ int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
- var message = new byte[messageLength];
+ // TODO: variable length: this can be 2 bytes if len > 127
+ // Write length in front of value
+ buffer[0] = Convert.ToByte(len);
- int offset = InsertHeaderAndName(byteName, messageLength, message);
+ // null-terminate
+ buffer[len++] = 0;
- bool flipEndian = BitConverter.IsLittleEndian;
+ return len;
+ }
- message[offset++] = GetSetValue;
- message[offset++] = Convert.ToByte(byteValue.Length);
- Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length);
- offset += byteValue.Length;
- if (lockkey.HasValue)
- {
- message[offset++] = GetSetLockkey;
- message[offset++] = 4;
- var lockKeyBytes = BitConverter.GetBytes(lockkey.Value);
- if (flipEndian)
- {
- Array.Reverse(lockKeyBytes);
- }
+ private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
+ {
+ // Packet type
+ BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
- Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4);
- offset += 4;
- }
+ // We write the payload length at the end
+ int offset = 4;
- // calculate crc and insert at the end of the message
- var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
- if (flipEndian)
- {
- Array.Reverse(crcBytes);
- }
+ // Tag
+ buffer[offset++] = GetSetName;
- Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
+ // Payload length + data
+ int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
+ offset += strLen;
- return message;
+ return offset;
}
- private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message)
+ private static int FinishPacket(Span<byte> buffer, int offset)
{
- // check to see if we need to flip endiannes
- bool flipEndian = BitConverter.IsLittleEndian;
- int offset = 0;
+ // Payload length
+ BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
- // create header bytes
- var getSetBytes = BitConverter.GetBytes(GetSetRequest);
- var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc
-
- if (flipEndian)
- {
- Array.Reverse(getSetBytes);
- Array.Reverse(msgLenBytes);
- }
-
- // insert header bytes into message
- Buffer.BlockCopy(getSetBytes, 0, message, offset, 2);
- offset += 2;
- Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2);
- offset += 2;
-
- // insert tag name and length
- message[offset++] = GetSetName;
- message[offset++] = Convert.ToByte(byteName.Length);
+ // calculate crc and insert at the end of the message
+ var crc = Crc32.Compute(buffer.Slice(0, offset));
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
- // insert name string
- Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
- offset += byteName.Length;
+ return offset + 4;
+ }
- return offset;
+ internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan<byte> buffer, string expected)
+ {
+ return TryGetReturnValueOfGetSet(buffer, out var value)
+ && string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase);
}
- private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
+ internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> value)
{
- returnVal = string.Empty;
+ value = ReadOnlySpan<byte>.Empty;
- if (numBytes < 4)
+ if (buffer.Length < 8)
{
return false;
}
- var flipEndian = BitConverter.IsLittleEndian;
- int offset = 0;
- byte[] msgTypeBytes = new byte[2];
- Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length);
-
- if (flipEndian)
+ uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]);
+ if (crc != Crc32.Compute(buffer[..^4]))
{
- Array.Reverse(msgTypeBytes);
+ return false;
}
- var msgType = BitConverter.ToUInt16(msgTypeBytes, 0);
- offset += 2;
-
- if (msgType != GetSetReply)
+ if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply)
{
return false;
}
- byte[] msgLengthBytes = new byte[2];
- Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length);
- if (flipEndian)
+ var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2));
+ if (buffer.Length != 2 + 2 + 4 + msgLength)
{
- Array.Reverse(msgLengthBytes);
+ return false;
}
- var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0);
- offset += 2;
-
- if (numBytes < msgLength + 8)
+ var offset = 4;
+ if (buffer[offset++] != GetSetName)
{
return false;
}
- offset++; // Name Tag
-
- var nameLength = buf[offset++];
+ var nameLength = buffer[offset++];
+ if (buffer.Length < 4 + 1 + offset + nameLength)
+ {
+ return false;
+ }
- // skip the name field to get to value for return
offset += nameLength;
- offset++; // Value Tag
-
- var valueLength = buf[offset++];
-
- returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
- return true;
- }
-
- private static class HdHomerunCrc
- {
- private static uint[] crc_table = {
- 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
- 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
- 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
- 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
- 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
- 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
- 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
- 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
- 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
- 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
- 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
- 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
- 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
- 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
- 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
- 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
- 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
- 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
- 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
- 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
- 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
- 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
- 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
- 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
- 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
- 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
- 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
- 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
- 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
- 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
- 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
- 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
- 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
- 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
- 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
- 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
- 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
- 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
- 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
- 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
- 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
- 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
- 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
- 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
- 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
- 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
- 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
- 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
- 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
- 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
- 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
- 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
- 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
- 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
- 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
- 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
- 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
- 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
- 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
- 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
- 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
- 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
- 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
- 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d };
-
- public static uint GetCrc32(byte[] bytes, int numBytes)
+ if (buffer[offset++] != GetSetValue)
{
- var hash = 0xffffffff;
- for (var i = 0; i < numBytes; i++)
- {
- hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
- }
+ return false;
+ }
- var tmp = ~hash & 0xffffffff;
- var b0 = tmp & 0xff;
- var b1 = (tmp >> 8) & 0xff;
- var b2 = (tmp >> 16) & 0xff;
- var b3 = (tmp >> 24) & 0xff;
- return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
+ var valueLength = buffer[offset++];
+ if (buffer.Length < 4 + offset + valueLength)
+ {
+ return false;
}
+
+ // remove null terminator
+ value = buffer.Slice(offset, valueLength - 1);
+ return true;
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index cf653f87d..58e0c7448 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,7 +12,6 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -28,7 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IServerApplicationHost _appHost;
private readonly IHdHomerunChannelCommands _channelCommands;
private readonly int _numTuners;
- private readonly INetworkManager _networkManager;
public HdHomerunUdpStream(
MediaSourceInfo mediaSource,
@@ -40,12 +40,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
ILogger logger,
IConfigurationManager configurationManager,
IServerApplicationHost appHost,
- INetworkManager networkManager,
IStreamHelper streamHelper)
: base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
{
_appHost = appHost;
- _networkManager = networkManager;
OriginalStreamId = originalStreamId;
_channelCommands = channelCommands;
_numTuners = numTuners;
@@ -92,7 +90,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
try
{
- await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
+ await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort, openCancellationToken).ConfigureAwait(false);
localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
tcpClient.Close();
}
@@ -126,7 +124,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using (udpClient)
using (hdHomerunManager)
{
- if (!(ex is OperationCanceledException))
+ if (ex is not OperationCanceledException)
{
Logger.LogError(ex, "Error opening live stream:");
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 78e62ff0a..96a678c1d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -150,7 +152,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
{
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token).Token;
+ using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
+ cancellationToken = linkedCancellationTokenSource.Token;
// use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039
var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 4b170b2e4..8fa6f5ad6 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -27,6 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
+ private static readonly string[] _disallowedSharedStreamExtensions =
+ {
+ ".mkv",
+ ".mp4",
+ ".m3u8",
+ ".mpd"
+ };
+
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
@@ -65,7 +75,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
- return await new M3uParser(Logger, _httpClientFactory, _appHost)
+ return await new M3uParser(Logger, _httpClientFactory)
.Parse(info, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
}
@@ -86,14 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- private static readonly string[] _disallowedSharedStreamExtensions =
- {
- ".mkv",
- ".mp4",
- ".m3u8",
- ".mpd"
- };
-
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = info.TunerCount;
@@ -128,7 +130,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
- using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
+ using (var stream = await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index c82b67b41..40a162890 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -1,10 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
@@ -20,15 +21,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3uParser
{
+ private const string ExtInfPrefix = "#EXTINF:";
+
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerApplicationHost _appHost;
- public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
+ public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
}
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
@@ -36,16 +37,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
// Read the file and display it line by line.
using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
{
- return GetChannels(reader, channelIdPrefix, info.Id);
- }
- }
-
- public List<ChannelInfo> ParseString(string text, string channelIdPrefix, string tunerHostId)
- {
- // Read the file and display it line by line.
- using (var reader = new StringReader(text))
- {
- return GetChannels(reader, channelIdPrefix, tunerHostId);
+ return await GetChannelsAsync(reader, channelIdPrefix, info.Id).ConfigureAwait(false);
}
}
@@ -69,45 +61,42 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return File.OpenRead(info.Url);
}
- private const string ExtInfPrefix = "#EXTINF:";
-
- private List<ChannelInfo> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId)
+ private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
{
var channels = new List<ChannelInfo>();
- string line;
string extInf = string.Empty;
- while ((line = reader.ReadLine()) != null)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- line = line.Trim();
- if (string.IsNullOrWhiteSpace(line))
+ var trimmedLine = line.Trim();
+ if (string.IsNullOrWhiteSpace(trimmedLine))
{
continue;
}
- if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
+ if (trimmedLine.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
{
continue;
}
- if (line.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
+ if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
{
- extInf = line.Substring(ExtInfPrefix.Length).Trim();
+ extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
_logger.LogInformation("Found m3u channel: {0}", extInf);
}
- else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith('#'))
+ else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
- var channel = GetChannelnfo(extInf, tunerHostId, line);
+ var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
if (string.IsNullOrWhiteSpace(channel.Id))
{
- channel.Id = channelIdPrefix + line.GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
else
{
channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
- channel.Path = line;
+ channel.Path = trimmedLine;
channels.Add(channel);
extInf = string.Empty;
}
@@ -133,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
channel.ImageUrl = value;
}
+ if (attributes.TryGetValue("group-title", out string groupTitle))
+ {
+ channel.ChannelGroup = groupTitle;
+ }
+
channel.Name = GetChannelName(extInf, attributes);
channel.Number = GetChannelNumber(extInf, attributes, mediaUrl);
@@ -155,7 +149,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (channelIdValues.Count > 0)
{
- channel.Id = string.Join("_", channelIdValues);
+ channel.Id = string.Join('_', channelIdValues);
}
return channel;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index f7507e6ba..f572151b8 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -89,8 +91,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var taskCompletionSource = new TaskCompletionSource<bool>();
- var now = DateTime.UtcNow;
-
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
// OpenedMediaSource.Protocol = MediaProtocol.File;
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!taskCompletionSource.Task.Result)
{
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
- throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
+ throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
}
}
@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
EnableStreamSharing = false;
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
- });
+ }, CancellationToken.None);
}
private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 977a1c2d7..4f21c66bc 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -112,5 +112,10 @@
"TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
"TaskRefreshLibrary": "Skandeer Media Versameling",
"TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
- "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
+ "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde",
+ "Undefined": "Ongedefineerd",
+ "Forced": "Geforseer",
+ "Default": "Oorspronklik",
+ "TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
+ "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 4b898e6fe..3d6e159b1 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -113,5 +113,10 @@
"TaskRefreshPeopleDescription": "تحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
"TaskRefreshPeople": "إعادة تحميل الأشخاص",
"TaskCleanLogsDescription": "حذف السجلات الأقدم من {0} يوم.",
- "TaskCleanLogs": "حذف دليل السجل"
+ "TaskCleanLogs": "حذف دليل السجل",
+ "TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الموضوع.",
+ "TaskCleanActivityLog": "حذف سجل الأنشطة",
+ "Default": "الإعدادات الافتراضية",
+ "Undefined": "غير معرف",
+ "Forced": "ملحقة"
}
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 9db3b50d9..bc25531d3 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -39,7 +39,7 @@
"MixedContent": "Смесено съдържание",
"Movies": "Филми",
"Music": "Музика",
- "MusicVideos": "Музикални клипове",
+ "MusicVideos": "Музикални видеа",
"NameInstallFailed": "{0} не можа да се инсталира",
"NameSeasonNumber": "Сезон {0}",
"NameSeasonUnknown": "Неразпознат сезон",
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно",
"Photos": "Снимки",
"Playlists": "Списъци",
- "Plugin": "Приставка",
+ "Plugin": "Добавка",
"PluginInstalledWithName": "{0} е инсталиранa",
"PluginUninstalledWithName": "{0} е деинсталиранa",
"PluginUpdatedWithName": "{0} е обновенa",
@@ -116,5 +116,7 @@
"TasksMaintenanceCategory": "Поддръжка",
"Undefined": "Неопределено",
"Forced": "Принудително",
- "Default": "По подразбиране"
+ "Default": "По подразбиране",
+ "TaskCleanActivityLogDescription": "Изтрива записите в дневника с активност по стари от конфигурираната възраст.",
+ "TaskCleanActivityLog": "Изчисти дневника с активност"
}
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index a23037af8..c3fbe2408 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -1,7 +1,7 @@
{
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
- "Collections": "কলেক্শন",
+ "Collections": "সংগ্রহ",
"ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেল",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
@@ -115,7 +115,7 @@
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
"Undefined": "অসঙ্গায়িত",
"Forced": "জোরকরে",
- "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন",
+ "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
"TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
"Default": "প্রাথমিক"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 775267183..ff14c1929 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -39,7 +39,7 @@
"MixedContent": "Smíšený obsah",
"Movies": "Filmy",
"Music": "Hudba",
- "MusicVideos": "Hudební klipy",
+ "MusicVideos": "Hudební videa",
"NameInstallFailed": "Instalace {0} selhala",
"NameSeasonNumber": "Sezóna {0}",
"NameSeasonUnknown": "Neznámá sezóna",
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 4ee4eb989..3453507d9 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -1,5 +1,5 @@
{
- "Albums": "Albums",
+ "Albums": "Albummer",
"AppDeviceValues": "App: {0}, Enhed: {1}",
"Application": "Applikation",
"Artists": "Kunstnere",
@@ -39,7 +39,7 @@
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
- "MusicVideos": "Musikvideoer",
+ "MusicVideos": "Musik videoer",
"NameInstallFailed": "{0} installationen mislykkedes",
"NameSeasonNumber": "Sæson {0}",
"NameSeasonUnknown": "Ukendt Sæson",
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index f8f595faa..65964f6d9 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -117,5 +117,7 @@
"TaskRefreshChannels": "Refresh Channels",
"TaskRefreshChannelsDescription": "Refreshes internet channel information.",
"TaskDownloadMissingSubtitles": "Download missing subtitles",
- "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration."
+ "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
+ "TaskOptimizeDatabase": "Optimize database",
+ "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance."
}
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index 3ff7eddae..ca615cc8c 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -22,5 +22,26 @@
"Artists": "Artistoj",
"Application": "Aplikaĵo",
"AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}",
- "Albums": "Albumoj"
+ "Albums": "Albumoj",
+ "TasksLibraryCategory": "Libraro",
+ "VersionNumber": "Versio {0}",
+ "UserDownloadingItemWithValues": "{0} elŝutas {1}",
+ "UserCreatedWithName": "Uzanto {0} kreiĝis",
+ "User": "Uzanto",
+ "System": "Sistemo",
+ "Songs": "Kantoj",
+ "ScheduledTaskStartedWithName": "{0} komencis",
+ "ScheduledTaskFailedWithName": "{0} malsukcesis",
+ "PluginUninstalledWithName": "{0} malinstaliĝis",
+ "PluginInstalledWithName": "{0} instaliĝis",
+ "Plugin": "Kromprogramo",
+ "Playlists": "Ludlistoj",
+ "Photos": "Fotoj",
+ "NotificationOptionPluginUninstalled": "Kromprogramo malinstaliĝis",
+ "NotificationOptionNewLibraryContent": "Nova enhavo aldoniĝis",
+ "NotificationOptionPluginInstalled": "Kromprogramo instaliĝis",
+ "MusicVideos": "Muzikvideoj",
+ "LabelIpAddressValue": "IP-adreso: {0}",
+ "Genres": "Ĝenroj",
+ "DeviceOfflineWithName": "{0} malkonektis"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 05181116d..5d7ed243f 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -117,5 +117,6 @@
"TaskCleanActivityLogDescription": "Elimina entradas del registro de actividad que sean más antiguas al periodo establecido.",
"TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir",
- "Forced": "Forzado"
+ "Forced": "Forzado",
+ "Default": "Predeterminado"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 16fde325f..91939843f 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpiar registro de actividad",
"Undefined": "Indefinido",
"Forced": "Forzado",
- "Default": "Predeterminado"
+ "Default": "Predeterminado",
+ "TaskOptimizeDatabase": "Optimizar la base de datos",
+ "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index e9e4f61b8..8ab657e5b 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -34,7 +34,7 @@
"Latest": "جدیدترین‌ها",
"MessageApplicationUpdated": "سرور Jellyfin بروزرسانی شد",
"MessageApplicationUpdatedTo": "سرور Jellyfin به نسخه {0} بروزرسانی شد",
- "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
+ "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
"MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
"MixedContent": "محتوای مخلوط",
"Movies": "فیلم‌ها",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index fd6148e78..633968d26 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -1,5 +1,5 @@
{
- "HeaderLiveTV": "Live TV",
+ "HeaderLiveTV": "Suora TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin-palvelimesta on ladattavissa.",
"NameSeasonUnknown": "Tuntematon kausi",
"NameSeasonNumber": "Kausi {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 1e195378f..ce1493be8 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -15,7 +15,7 @@
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes",
+ "HeaderAlbumArtists": "Artistes de l'album",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
@@ -39,7 +39,7 @@
"MixedContent": "Contenu mixte",
"Movies": "Films",
"Music": "Musique",
- "MusicVideos": "Vidéos musicales",
+ "MusicVideos": "Clips musicaux",
"NameInstallFailed": "{0} échec de l'installation",
"NameSeasonNumber": "Saison {0}",
"NameSeasonUnknown": "Saison Inconnue",
@@ -99,7 +99,7 @@
"TaskRefreshChannels": "Rafraîchir les chaines",
"TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",
"TaskCleanTranscode": "Nettoyer les dossier des transcodages",
- "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour être mises à jour automatiquement.",
+ "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurées pour être mises à jour automatiquement.",
"TaskUpdatePlugins": "Mettre à jour les extensions",
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
"TaskRefreshPeople": "Rafraîchir les acteurs",
@@ -107,7 +107,7 @@
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
"TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
- "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
+ "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
"TaskCleanCache": "Vider le répertoire cache",
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index faee2519a..0398e1c9e 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -7,5 +7,86 @@
"Books": "Libros",
"AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
"Artists": "Artistas",
- "Application": "Aplicativo"
+ "Application": "Aplicativo",
+ "NotificationOptionServerRestartRequired": "Necesario un reinicio do servidor",
+ "NotificationOptionPluginUpdateInstalled": "Actualización do Plugin instalada",
+ "NotificationOptionPluginUninstalled": "Plugin desinstalado",
+ "NotificationOptionPluginInstalled": "Plugin instalado",
+ "NotificationOptionPluginError": "Fallo do Plugin",
+ "NotificationOptionNewLibraryContent": "Novo contido engadido",
+ "NotificationOptionInstallationFailed": "Fallo na instalación",
+ "NotificationOptionCameraImageUploaded": "Imaxe da cámara subida",
+ "NotificationOptionAudioPlaybackStopped": "Reproducción de audio parada",
+ "NotificationOptionAudioPlayback": "Reproducción de audio comezada",
+ "NotificationOptionApplicationUpdateInstalled": "Actualización da aplicación instalada",
+ "NotificationOptionApplicationUpdateAvailable": "Actualización da aplicación dispoñible",
+ "NewVersionIsAvailable": "Unha nova versión do Servidor Jellyfin está dispoñible para descarga.",
+ "NameSeasonUnknown": "Tempada descoñecida",
+ "NameSeasonNumber": "Tempada {0}",
+ "NameInstallFailed": "{0} instalación fallida",
+ "MusicVideos": "Vídeos Musicais",
+ "Music": "Música",
+ "Movies": "Películas",
+ "MixedContent": "Contido Mixto",
+ "MessageServerConfigurationUpdated": "A configuración do servidor foi actualizada",
+ "MessageNamedServerConfigurationUpdatedWithValue": "A sección de configuración {0} do servidor foi actualizada",
+ "MessageApplicationUpdatedTo": "O servidor Jellyfin foi actualizado a {0}",
+ "MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
+ "Latest": "Último",
+ "LabelRunningTimeValue": "Tempo de execución: {0}",
+ "LabelIpAddressValue": "Enderezo IP: {0}",
+ "ItemRemovedWithName": "{0} foi eliminado da biblioteca",
+ "ItemAddedWithName": "{0} foi engadido a biblioteca",
+ "Inherit": "Herdar",
+ "HomeVideos": "Videos caseiros",
+ "HeaderRecordingGroups": "Grupos de Grabación",
+ "HeaderNextUp": "De seguido",
+ "HeaderLiveTV": "TV en directo",
+ "HeaderFavoriteSongs": "Cancións Favoritas",
+ "HeaderFavoriteShows": "Series de TV Favoritas",
+ "HeaderFavoriteEpisodes": "Episodios Favoritos",
+ "HeaderFavoriteArtists": "Artistas Favoritos",
+ "HeaderFavoriteAlbums": "Álbunes Favoritos",
+ "HeaderContinueWatching": "Seguir mirando",
+ "HeaderAlbumArtists": "Artistas de Album",
+ "Genres": "Xéneros",
+ "Forced": "Forzado",
+ "Folders": "Cartafoles",
+ "Favorites": "Favoritos",
+ "FailedLoginAttemptWithUserName": "Intento de incio de sesión fallido {0}",
+ "DeviceOnlineWithName": "{0} conectouse",
+ "DeviceOfflineWithName": "{0} desconectouse",
+ "Default": "Por defecto",
+ "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
+ "TaskCleanLogs": "Limpar Carpeta de Rexistros",
+ "TaskCleanActivityLog": "Limpar Rexistro de Actividade",
+ "TasksChannelsCategory": "Canáis de Internet",
+ "TaskUpdatePlugins": "Actualizar Plugins",
+ "User": "Usuario",
+ "Undefined": "Sen definir",
+ "TvShows": "Programas de TV",
+ "System": "Sistema",
+ "Sync": "Sincronizar",
+ "SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}",
+ "StartupEmbyServerIsLoading": "O Servidor Jellyfin está cargando. Por favor, reinténteo en breve.",
+ "Songs": "Cancións",
+ "Shows": "Programas",
+ "ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado",
+ "ScheduledTaskStartedWithName": "{0} comezou",
+ "ScheduledTaskFailedWithName": "{0} fallou",
+ "ProviderValue": "Provedor: {0}",
+ "PluginUpdatedWithName": "{0} foi actualizado",
+ "PluginUninstalledWithName": "{0} foi desinstalado",
+ "PluginInstalledWithName": "{0} foi instalado",
+ "Playlists": "Listas de reproducción",
+ "Photos": "Fotos",
+ "UserLockedOutWithName": "O usuario {0} foi bloqueado",
+ "UserDownloadingItemWithValues": "{0} está a ser transferido {1}",
+ "UserDeletedWithName": "O usuario {0} foi borrado",
+ "UserCreatedWithName": "O usuario {0} foi creado",
+ "Plugin": "Plugin",
+ "NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada",
+ "NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
+ "NotificationOptionUserLockedOut": "Usuario bloqueado",
+ "NotificationOptionTaskFailed": "Falla na tarefa axendada"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index ef3697b15..82dc601bc 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -51,5 +51,14 @@
"Latest": "सबसे नया",
"LabelIpAddressValue": "आई पी एड्रेस: {0}",
"ItemRemovedWithName": "{0} लाइब्रेरी में से निकाल दिया है",
- "HomeVideos": "होम वीडियोस"
+ "HomeVideos": "होम वीडियोस",
+ "NotificationOptionVideoPlayback": "वीडियो प्लेबैक शुरू हुआ",
+ "NotificationOptionUserLockedOut": "उपयोगकर्ता लॉक हो गया",
+ "NotificationOptionTaskFailed": "निर्धारित कार्य विफलता",
+ "NotificationOptionServerRestartRequired": "सर्वर पुनरारंभ आवश्यक है",
+ "NotificationOptionPluginUpdateInstalled": "प्लगइन अद्यतन स्थापित",
+ "NotificationOptionNewLibraryContent": "नई सामग्री जोड़ी गई",
+ "LabelRunningTimeValue": "चलने का समय: {0}",
+ "ItemAddedWithName": "{0} को लाइब्रेरी में जोड़ा गया",
+ "Inherit": "इनहेरिट"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index e5707e78c..85848fed6 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -39,7 +39,7 @@
"MixedContent": "Vegyes tartalom",
"Movies": "Filmek",
"Music": "Zene",
- "MusicVideos": "Zenei videók",
+ "MusicVideos": "Zenei videóklippek",
"NameInstallFailed": "{0} sikertelen telepítés",
"NameSeasonNumber": "{0}. évad",
"NameSeasonUnknown": "Ismeretlen évad",
@@ -49,7 +49,7 @@
"NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
"NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva",
"NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
- "NotificationOptionInstallationFailed": "Telepítési hiba",
+ "NotificationOptionInstallationFailed": "Telepítés sikertelen",
"NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
"NotificationOptionPluginError": "Bővítmény hiba",
"NotificationOptionPluginInstalled": "Bővítmény telepítve",
@@ -74,7 +74,7 @@
"Songs": "Dalok",
"StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}",
+ "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}",
"Sync": "Szinkronizál",
"System": "Rendszer",
"TvShows": "TV műsorok",
@@ -82,12 +82,12 @@
"UserCreatedWithName": "{0} felhasználó létrehozva",
"UserDeletedWithName": "{0} felhasználó törölve",
"UserDownloadingItemWithValues": "{0} letölti {1}",
- "UserLockedOutWithName": "{0} felhasználó zárolva van",
- "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
+ "UserLockedOutWithName": "{0} felhasználó zárolva van",
+ "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
"UserOnlineFromDevice": "{0} online innen: {1}",
"UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}",
"UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}",
- "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}",
+ "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}",
"UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}",
"ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
"ValueSpecialEpisodeName": "Special - {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 105ef7be9..ba3513870 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -1,7 +1,7 @@
{
"Albums": "Album",
"AuthenticationSucceededWithUserName": "{0} berhasil diautentikasi",
- "AppDeviceValues": "Aplikasi : {0}, Alat : {1}",
+ "AppDeviceValues": "Aplikasi : {0}, Perangkat : {1}",
"LabelRunningTimeValue": "Waktu berjalan: {0}",
"MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}",
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 0f769eaad..b262a8b42 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -25,7 +25,7 @@
"Channels": "Stöðvar",
"CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}",
"Books": "Bækur",
- "AuthenticationSucceededWithUserName": "{0} náði að auðkennast",
+ "AuthenticationSucceededWithUserName": "{0} auðkenning tókst",
"Artists": "Listamaður",
"Application": "Forrit",
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
@@ -106,5 +106,6 @@
"TasksChannelsCategory": "Netrásir",
"TasksApplicationCategory": "Forrit",
"TasksLibraryCategory": "Miðlasafn",
- "TasksMaintenanceCategory": "Viðhald"
+ "TasksMaintenanceCategory": "Viðhald",
+ "Default": "Sjálfgefið"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 110f8043d..bd06f0a25 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta",
"Photos": "Foto",
"Playlists": "Playlist",
- "Plugin": "Plug-in",
+ "Plugin": "Plugin",
"PluginInstalledWithName": "{0} è stato Installato",
"PluginUninstalledWithName": "{0} è stato disinstallato",
"PluginUpdatedWithName": "{0} è stato aggiornato",
@@ -87,7 +87,7 @@
"UserOnlineFromDevice": "{0} è online su {1}",
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
- "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
+ "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di \"{1}\" su {2}",
"UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}",
"ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale",
"ValueSpecialEpisodeName": "Speciale - {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index a321e35d0..4eee36989 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -5,23 +5,23 @@
"Artists": "Oryndauşylar",
"AuthenticationSucceededWithUserName": "{0} tüpnūsqalyq rastaluy sättı aiaqtaldy",
"Books": "Kıtaptar",
- "CameraImageUploadedFrom": "{0} kamerasynan jaŋa suret jüktep salyndy",
+ "CameraImageUploadedFrom": "{0} kamerasynan jaña suret jüktep salyndy",
"Channels": "Arnalar",
"ChapterNameValue": "{0}-sahna",
"Collections": "Jiyntyqtar",
"DeviceOfflineWithName": "{0} ajyratylğan",
"DeviceOnlineWithName": "{0} qosylğan",
"FailedLoginAttemptWithUserName": "{0} tarapynan kıru äreketı sätsız aiaqtaldy",
- "Favorites": "Taŋdaulylar",
+ "Favorites": "Tañdaulylar",
"Folders": "Qaltalar",
"Genres": "Janrlar",
"HeaderAlbumArtists": "Älbom oryndauşylary",
"HeaderContinueWatching": "Qaraudy jalğastyru",
- "HeaderFavoriteAlbums": "Taŋdauly älbomdar",
- "HeaderFavoriteArtists": "Taŋdauly oryndauşylar",
- "HeaderFavoriteEpisodes": "Taŋdauly telebölımder",
- "HeaderFavoriteShows": "Taŋdauly körsetımder",
- "HeaderFavoriteSongs": "Taŋdauly äuender",
+ "HeaderFavoriteAlbums": "Tañdauly älbomdar",
+ "HeaderFavoriteArtists": "Tañdauly oryndauşylar",
+ "HeaderFavoriteEpisodes": "Tañdauly telebölımder",
+ "HeaderFavoriteShows": "Tañdauly körsetımder",
+ "HeaderFavoriteSongs": "Tañdauly äuender",
"HeaderLiveTV": "Efir",
"HeaderNextUp": "Kezektı",
"HeaderRecordingGroups": "Jazba toptary",
@@ -31,11 +31,11 @@
"ItemRemovedWithName": "{0} tasyğyşhanadan alastaldy",
"LabelIpAddressValue": "IP-mekenjaiy: {0}",
"LabelRunningTimeValue": "Oinatu uaqyty: {0}",
- "Latest": "Eŋ keiıngı",
- "MessageApplicationUpdated": "Jellyfin Serverı jaŋartyldy",
- "MessageApplicationUpdatedTo": "Jellyfin Serverı {0} nūsqasyna jaŋartyldy",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server teŋşelımderınıŋ {0} bölımı jaŋartyldy",
- "MessageServerConfigurationUpdated": "Server teŋşelımderı jaŋartyldy",
+ "Latest": "Eñ keiıngı",
+ "MessageApplicationUpdated": "Jellyfin Serverı jañartyldy",
+ "MessageApplicationUpdatedTo": "Jellyfin Serverı {0} nūsqasyna jañartyldy",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Server teñşelımderınıñ {0} bölımı jañartyldy",
+ "MessageServerConfigurationUpdated": "Server teñşelımderı jañartyldy",
"MixedContent": "Aralas mazmūn",
"Movies": "Filmder",
"Music": "Muzyka",
@@ -43,18 +43,18 @@
"NameInstallFailed": "{0} ornatyluy sätsız",
"NameSeasonNumber": "{0}-mausym",
"NameSeasonUnknown": "Belgısız mausym",
- "NewVersionIsAvailable": "Jaŋa Jellyfin Server nūsqasy jüktep aluğa qoljetımdı.",
- "NotificationOptionApplicationUpdateAvailable": "Qoldanba jaŋartuy qoljetımdı",
- "NotificationOptionApplicationUpdateInstalled": "Qoldanba jaŋartuy ornatyldy",
+ "NewVersionIsAvailable": "Jaña Jellyfin Server nūsqasy jüktep aluğa qoljetımdı.",
+ "NotificationOptionApplicationUpdateAvailable": "Qoldanba jañartuy qoljetımdı",
+ "NotificationOptionApplicationUpdateInstalled": "Qoldanba jañartuy ornatyldy",
"NotificationOptionAudioPlayback": "Dybys oinatuy bastaldy",
"NotificationOptionAudioPlaybackStopped": "Dybys oinatuy toqtatyldy",
"NotificationOptionCameraImageUploaded": "Kameradan fotosuret jüktep salynğan",
"NotificationOptionInstallationFailed": "Ornatu sätsızdıgı",
- "NotificationOptionNewLibraryContent": "Jaŋa mazmūn üstelıngen",
+ "NotificationOptionNewLibraryContent": "Jaña mazmūn üstelıngen",
"NotificationOptionPluginError": "Plagin sätsızdıgı",
"NotificationOptionPluginInstalled": "Plagin ornatyldy",
"NotificationOptionPluginUninstalled": "Plagin ornatuy boldyrylmady",
- "NotificationOptionPluginUpdateInstalled": "Plagin jaŋartuy ornatyldy",
+ "NotificationOptionPluginUpdateInstalled": "Plagin jañartuy ornatyldy",
"NotificationOptionServerRestartRequired": "Serverdı qaita ıske qosu qajet",
"NotificationOptionTaskFailed": "Josparlağan tapsyrma sätsızdıgı",
"NotificationOptionUserLockedOut": "Paidalanuşy qūrsauly",
@@ -65,14 +65,14 @@
"Plugin": "Plagin",
"PluginInstalledWithName": "{0} ornatyldy",
"PluginUninstalledWithName": "{0} joiyldy",
- "PluginUpdatedWithName": "{0} jaŋartyldy",
+ "PluginUpdatedWithName": "{0} jañartyldy",
"ProviderValue": "Jetkızuşı: {0}",
"ScheduledTaskFailedWithName": "{0} sätsız",
"ScheduledTaskStartedWithName": "{0} ıske qosyldy",
"ServerNameNeedsToBeRestarted": "{0} qaita ıske qosu qajet",
"Shows": "Körsetımder",
"Songs": "Äuender",
- "StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalaŋyz.",
+ "StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalañyz.",
"SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз",
"SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız",
"Sync": "Ündestıru",
@@ -86,7 +86,7 @@
"UserOfflineFromDevice": "{0} — {1} tarapynan ajyratyldy",
"UserOnlineFromDevice": "{0} — {1} tarapynan qosyldy",
"UserPasswordChangedWithName": "Paidalanuşy {0} üşın paröl özgertıldı",
- "UserPolicyUpdatedWithName": "Paidalanuşy {0} üşın saiasattary jaŋartyldy",
+ "UserPolicyUpdatedWithName": "Paidalanuşy {0} üşın saiasattary jañartyldy",
"UserStartedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuda",
"UserStoppedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuyn toqtatty",
"ValueHasBeenAddedToLibrary": "{0} tasyğyşhanağa üstelındı",
@@ -94,10 +94,10 @@
"VersionNumber": "Nūsqasy {0}",
"Default": "Ädepkı",
"TaskDownloadMissingSubtitles": "Joq subtitrlerdı jüktep alu",
- "TaskRefreshChannels": "Arnalardy jaŋğyrtu",
+ "TaskRefreshChannels": "Arnalardy jañğyrtu",
"TaskCleanTranscode": "Qaita kodtau katalogyn tazalau",
- "TaskUpdatePlugins": "Plaginderdı jaŋartu",
- "TaskRefreshPeople": "Adamdardy jaŋğyrtu",
+ "TaskUpdatePlugins": "Plaginderdı jañartu",
+ "TaskRefreshPeople": "Adamdardy jañğyrtu",
"TaskCleanLogs": "Jūrnal katalogyn tazalau",
"TaskRefreshLibrary": "Tasyğyşhanany skanerleu",
"TaskRefreshChapterImages": "Sahna suretterın şyğaryp alu",
@@ -109,14 +109,14 @@
"TasksMaintenanceCategory": "Qyzmet körsetu",
"Undefined": "Anyqtalmağan",
"Forced": "Mäjbürlı",
- "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı Internetten ızdeidı.",
- "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.",
+ "TaskDownloadMissingSubtitlesDescription": "Metaderekter teñşelımderı negızınde joq subtitrlerdı İnternetten ızdeidı.",
+ "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jañğyrtady.",
"TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.",
- "TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.",
- "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.",
+ "TaskUpdatePluginsDescription": "Avtomatty türde jañartuğa teñşelgen plaginder üşın jañartulardy jüktep alady jäne ornatady.",
+ "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jañartady.",
"TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.",
- "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.",
- "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşіn nobailar jasaidy.",
+ "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaña faildardy skanerleidі jäne metaderekterdı jañğyrtady.",
+ "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.",
"TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
- "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady."
+ "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index d4cb592ef..f3a131d40 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -113,5 +113,10 @@
"TasksChannelsCategory": "Internetiniai Kanalai",
"TasksApplicationCategory": "Programa",
"TasksLibraryCategory": "Mediateka",
- "TasksMaintenanceCategory": "Priežiūra"
+ "TasksMaintenanceCategory": "Priežiūra",
+ "TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
+ "Undefined": "Neapibrėžtas",
+ "Forced": "Priverstas",
+ "Default": "Numatytas",
+ "TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 5e3d095ff..5b4c8ae10 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -39,29 +39,29 @@
"MixedContent": "Kandungan campuran",
"Movies": "Filem",
"Music": "Muzik",
- "MusicVideos": "Video muzik",
+ "MusicVideos": "Muzik video",
"NameInstallFailed": "{0} pemasangan gagal",
"NameSeasonNumber": "Musim {0}",
"NameSeasonUnknown": "Musim Tidak Diketahui",
"NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
"NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
- "NotificationOptionApplicationUpdateInstalled": "Application update installed",
- "NotificationOptionAudioPlayback": "Audio playback started",
- "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
- "NotificationOptionCameraImageUploaded": "Camera image uploaded",
+ "NotificationOptionApplicationUpdateInstalled": "Kemas kini aplikasi telah dipasang",
+ "NotificationOptionAudioPlayback": "Ulangmain audio bermula",
+ "NotificationOptionAudioPlaybackStopped": "Ulangmain audio dihentikan",
+ "NotificationOptionCameraImageUploaded": "Imej kamera telah dimuatnaik",
"NotificationOptionInstallationFailed": "Pemasangan gagal",
- "NotificationOptionNewLibraryContent": "New content added",
- "NotificationOptionPluginError": "Plugin failure",
- "NotificationOptionPluginInstalled": "Plugin installed",
+ "NotificationOptionNewLibraryContent": "Kandungan baru telah ditambah",
+ "NotificationOptionPluginError": "Kegagalan plugin",
+ "NotificationOptionPluginInstalled": "Plugin telah dipasang",
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
"NotificationOptionServerRestartRequired": "Server restart required",
"NotificationOptionTaskFailed": "Scheduled task failure",
"NotificationOptionUserLockedOut": "User locked out",
"NotificationOptionVideoPlayback": "Video playback started",
- "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
- "Photos": "Photos",
- "Playlists": "Playlists",
+ "NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
+ "Photos": "Gambar-gambar",
+ "Playlists": "Senarai main",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
@@ -71,10 +71,10 @@
"ScheduledTaskStartedWithName": "{0} bermula",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"Shows": "Series",
- "Songs": "Songs",
- "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
+ "Songs": "Lagu-lagu",
+ "StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+ "SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}",
"Sync": "Sync",
"System": "Sistem",
"TvShows": "TV Shows",
@@ -82,14 +82,24 @@
"UserCreatedWithName": "User {0} has been created",
"UserDeletedWithName": "User {0} has been deleted",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
- "UserLockedOutWithName": "User {0} has been locked out",
- "UserOfflineFromDevice": "{0} has disconnected from {1}",
- "UserOnlineFromDevice": "{0} is online from {1}",
- "UserPasswordChangedWithName": "Password has been changed for user {0}",
- "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
+ "UserLockedOutWithName": "Pengguna {0} telah dikunci",
+ "UserOfflineFromDevice": "{0} telah terputus dari {1}",
+ "UserOnlineFromDevice": "{0} berada dalam talian dari {1}",
+ "UserPasswordChangedWithName": "Kata laluan telah ditukar bagi pengguna {0}",
+ "UserPolicyUpdatedWithName": "Dasar pengguna telah dikemas kini untuk {0}",
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueSpecialEpisodeName": "Khas - {0}",
- "VersionNumber": "Versi {0}"
+ "VersionNumber": "Versi {0}",
+ "TaskCleanActivityLog": "Log Aktiviti Bersih",
+ "TasksChannelsCategory": "Saluran Internet",
+ "TasksApplicationCategory": "Aplikasi",
+ "TasksLibraryCategory": "Perpustakaan",
+ "TasksMaintenanceCategory": "Penyelenggaraan",
+ "Undefined": "Tidak ditentukan",
+ "Forced": "Paksa",
+ "Default": "Asal",
+ "TaskCleanCache": "Bersihkan Direktori Cache",
+ "TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index d5bca9f6c..fbe1f7c4d 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -30,20 +30,20 @@
"ItemAddedWithName": "{0} ble lagt til i biblioteket",
"ItemRemovedWithName": "{0} ble fjernet fra biblioteket",
"LabelIpAddressValue": "IP-adresse: {0}",
- "LabelRunningTimeValue": "Kjøretid {0}",
+ "LabelRunningTimeValue": "Spilletid {0}",
"Latest": "Siste",
- "MessageApplicationUpdated": "Jellyfin Server har blitt oppdatert",
- "MessageApplicationUpdatedTo": "Jellyfin Server ble oppdatert til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurasjon seksjon {0} har blitt oppdatert",
- "MessageServerConfigurationUpdated": "Serverkonfigurasjon er oppdatert",
+ "MessageApplicationUpdated": "Jellyfin-tjeneren har blitt oppdatert",
+ "MessageApplicationUpdatedTo": "Jellyfin-tjeneren ble oppdatert til {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Tjenerkonfigurasjonsseksjon {0} har blitt oppdatert",
+ "MessageServerConfigurationUpdated": "Tjenerkonfigurasjon er oppdatert",
"MixedContent": "Blandet innhold",
"Movies": "Filmer",
"Music": "Musikk",
"MusicVideos": "Musikkvideoer",
- "NameInstallFailed": "{0}-installasjonen mislyktes",
+ "NameInstallFailed": "Installasjonen av {0} mislyktes",
"NameSeasonNumber": "Sesong {0}",
- "NameSeasonUnknown": "Sesong ukjent",
- "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
+ "NameSeasonUnknown": "Ukjent sesong",
+ "NewVersionIsAvailable": "En ny versjon av Jellyfin-tjeneren er tilgjengelig for nedlasting.",
"NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
"NotificationOptionAudioPlayback": "Lydavspilling startet",
@@ -51,18 +51,18 @@
"NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
"NotificationOptionInstallationFailed": "Installasjonen feilet",
"NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
- "NotificationOptionPluginError": "Pluginfeil",
- "NotificationOptionPluginInstalled": "Plugin installert",
- "NotificationOptionPluginUninstalled": "Plugin avinstallert",
- "NotificationOptionPluginUpdateInstalled": "Pluginoppdatering installert",
- "NotificationOptionServerRestartRequired": "Serveromstart er nødvendig",
+ "NotificationOptionPluginError": "Programvareutvidelsesfeil",
+ "NotificationOptionPluginInstalled": "Programvareutvidelse installert",
+ "NotificationOptionPluginUninstalled": "Programvareutvidelse avinstallert",
+ "NotificationOptionPluginUpdateInstalled": "Programvareutvidelsesoppdatering installert",
+ "NotificationOptionServerRestartRequired": "Tjeneromstart er nødvendig",
"NotificationOptionTaskFailed": "Feil under utføring av planlagt oppgave",
"NotificationOptionUserLockedOut": "Bruker er utestengt",
"NotificationOptionVideoPlayback": "Videoavspilling startet",
"NotificationOptionVideoPlaybackStopped": "Videoavspilling stoppet",
"Photos": "Bilder",
"Playlists": "Spillelister",
- "Plugin": "Plugin",
+ "Plugin": "Programvareutvidelse",
"PluginInstalledWithName": "{0} ble installert",
"PluginUninstalledWithName": "{0} ble avinstallert",
"PluginUpdatedWithName": "{0} ble oppdatert",
@@ -72,7 +72,7 @@
"ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
"Shows": "Program",
"Songs": "Sanger",
- "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
+ "StartupEmbyServerIsLoading": "Jellyfin-tjener laster. Prøv igjen snart.",
"SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}",
"Sync": "Synkroniser",
@@ -86,37 +86,37 @@
"UserOfflineFromDevice": "{0} har koblet fra {1}",
"UserOnlineFromDevice": "{0} er tilkoblet fra {1}",
"UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
- "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}",
+ "UserPolicyUpdatedWithName": "Brukerretningslinjene har blitt oppdatert for {0}",
"UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}",
- "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}",
+ "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}",
"ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
"ValueSpecialEpisodeName": "Spesialepisode - {0}",
"VersionNumber": "Versjon {0}",
- "TasksChannelsCategory": "Internett kanaler",
+ "TasksChannelsCategory": "Internettkanaler",
"TasksApplicationCategory": "Applikasjon",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedlikehold",
- "TaskCleanCache": "Tøm buffer katalog",
+ "TaskCleanCache": "Tøm hurtigbuffer",
"TaskRefreshLibrary": "Skann mediebibliotek",
"TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.",
- "TaskRefreshChapterImages": "Trekk ut Kapittelbilder",
+ "TaskRefreshChapterImages": "Trekk ut kapittelbilder",
"TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet.",
- "TaskDownloadMissingSubtitlesDescription": "Søker etter manglende underteksting på nett basert på metadatakonfigurasjon.",
- "TaskDownloadMissingSubtitles": "Last ned manglende underteksting",
- "TaskRefreshChannelsDescription": "Frisker opp internettkanalinformasjon.",
- "TaskRefreshChannels": "Oppfrisk kanaler",
+ "TaskDownloadMissingSubtitlesDescription": "Søker etter manglende undertekster på nett basert på metadatakonfigurasjon.",
+ "TaskDownloadMissingSubtitles": "Last ned manglende undertekster",
+ "TaskRefreshChannelsDescription": "Oppdaterer internettkanalinformasjon.",
+ "TaskRefreshChannels": "Oppdater kanaler",
"TaskCleanTranscodeDescription": "Sletter omkodede filer som er mer enn én dag gamle.",
"TaskCleanTranscode": "Tøm transkodingmappe",
- "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringer for utvidelser som er stilt inn til å oppdatere automatisk.",
- "TaskUpdatePlugins": "Oppdater utvidelser",
+ "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringer for programvareutvidelser som er stilt inn til å oppdatere automatisk.",
+ "TaskUpdatePlugins": "Oppdater programvareutvidelse",
"TaskRefreshPeopleDescription": "Oppdaterer metadata for skuespillere og regissører i mediebiblioteket ditt.",
- "TaskRefreshPeople": "Oppfrisk personer",
+ "TaskRefreshPeople": "Oppdater personer",
"TaskCleanLogsDescription": "Sletter loggfiler som er eldre enn {0} dager gamle.",
"TaskCleanLogs": "Tøm loggmappe",
"TaskRefreshLibraryDescription": "Skanner mediebibliotekene dine for nye filer og oppdaterer metadata.",
"TaskCleanActivityLog": "Tøm aktivitetslogg",
"Undefined": "Udefinert",
- "Forced": "Tvungen",
+ "Forced": "Tvunget",
"Default": "Standard",
"TaskCleanActivityLogDescription": "Sletter oppføringer i aktivitetsloggen som er eldre enn den konfigurerte alderen."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index ffc329e35..2973c8c6e 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "App: {0}, Apparaat: {1}",
"Application": "Applicatie",
"Artists": "Artiesten",
- "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
+ "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
"Books": "Boeken",
- "CameraImageUploadedFrom": "Er is een nieuwe camera afbeelding toegevoegd via {0}",
+ "CameraImageUploadedFrom": "Nieuwe camera afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen",
"ChapterNameValue": "Hoofdstuk {0}",
"Collections": "Verzamelingen",
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index 6236515b2..32d4f3a8b 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -1,25 +1,25 @@
{
- "MessageServerConfigurationUpdated": "Tenar konfigurasjonen har blitt oppdatert",
+ "MessageServerConfigurationUpdated": "Tenarkonfigurasjonen har blitt oppdatert",
"MessageNamedServerConfigurationUpdatedWithValue": "Tenar konfigurasjon seksjon {0} har blitt oppdatert",
- "MessageApplicationUpdatedTo": "Jellyfin Tenaren har blitt oppdatert til {0}",
- "MessageApplicationUpdated": "Jellyfin Tenaren har blitt oppdatert",
+ "MessageApplicationUpdatedTo": "Jellyfin-tenaren har blitt oppdatert til {0}",
+ "MessageApplicationUpdated": "Jellyfin-tenaren har blitt oppdatert",
"Latest": "Nyaste",
"LabelRunningTimeValue": "Speletid: {0}",
- "LabelIpAddressValue": "IP adresse: {0}",
+ "LabelIpAddressValue": "IP-adresse: {0}",
"ItemRemovedWithName": "{0} vart fjerna frå biblioteket",
"ItemAddedWithName": "{0} vart lagt til i biblioteket",
- "Inherit": "Arv",
- "HomeVideos": "Heime Videoar",
+ "Inherit": "Arve",
+ "HomeVideos": "Heimevideoar",
"HeaderRecordingGroups": "Innspelingsgrupper",
"HeaderNextUp": "Neste",
"HeaderLiveTV": "Direkte TV",
- "HeaderFavoriteSongs": "Favoritt Songar",
- "HeaderFavoriteShows": "Favoritt Seriar",
- "HeaderFavoriteEpisodes": "Favoritt Episodar",
- "HeaderFavoriteArtists": "Favoritt Artistar",
- "HeaderFavoriteAlbums": "Favoritt Album",
+ "HeaderFavoriteSongs": "Favorittsongar",
+ "HeaderFavoriteShows": "Favorittseriar",
+ "HeaderFavoriteEpisodes": "Favorittepisodar",
+ "HeaderFavoriteArtists": "Favorittartistar",
+ "HeaderFavoriteAlbums": "Favorittalbum",
"HeaderContinueWatching": "Fortsett å sjå",
- "HeaderAlbumArtists": "Album Artist",
+ "HeaderAlbumArtists": "Albumartist",
"Genres": "Sjangrar",
"Folders": "Mapper",
"Favorites": "Favorittar",
@@ -29,18 +29,18 @@
"Collections": "Samlingar",
"ChapterNameValue": "Kapittel {0}",
"Channels": "Kanalar",
- "CameraImageUploadedFrom": "Eit nytt kamera bilete har blitt lasta opp frå {0}",
+ "CameraImageUploadedFrom": "Eit nytt kamerabilete har blitt lasta opp frå {0}",
"Books": "Bøker",
- "AuthenticationSucceededWithUserName": "{0} Har logga inn",
+ "AuthenticationSucceededWithUserName": "{0} har logga inn",
"Artists": "Artistar",
"Application": "Program",
"AppDeviceValues": "App: {0}, Eining: {1}",
"Albums": "Album",
"NotificationOptionServerRestartRequired": "Tenaren krev omstart",
- "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
- "NotificationOptionPluginUninstalled": "Tilleggsprogram avinstallert",
- "NotificationOptionPluginInstalled": "Tilleggsprogram installert",
- "NotificationOptionPluginError": "Tilleggsprogram feila",
+ "NotificationOptionPluginUpdateInstalled": "Programvaretilleggoppdatering vart installert",
+ "NotificationOptionPluginUninstalled": "Programvaretillegg avinstallert",
+ "NotificationOptionPluginInstalled": "Programvaretillegg installert",
+ "NotificationOptionPluginError": "Programvaretillegg feila",
"NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
"NotificationOptionInstallationFailed": "Installasjonsfeil",
"NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
@@ -48,33 +48,33 @@
"NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering er installert",
"NotificationOptionApplicationUpdateAvailable": "Applikasjonsoppdatering er tilgjengeleg",
- "NewVersionIsAvailable": "Ein ny versjon av Jellyfin serveren er tilgjengeleg for nedlasting.",
+ "NewVersionIsAvailable": "Ein ny versjon av Jellyfin-tjenaren er tilgjengeleg for nedlasting.",
"NameSeasonUnknown": "Ukjend sesong",
"NameSeasonNumber": "Sesong {0}",
- "NameInstallFailed": "{0} Installasjonen feila",
+ "NameInstallFailed": "Installasjonen av {0} feila",
"MusicVideos": "Musikkvideoar",
"Music": "Musikk",
"Movies": "Filmar",
"MixedContent": "Blanda innhald",
- "Sync": "Synkronisera",
+ "Sync": "Synkroniser",
"TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.",
"TaskDownloadMissingSubtitles": "Last ned manglande undertekstar",
"TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.",
"TaskRefreshChannels": "Oppdater kanalar",
- "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.",
- "TaskCleanTranscode": "Reins transkodemappe",
+ "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gammal.",
+ "TaskCleanTranscode": "Fjern transkodemappe",
"TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.",
- "TaskUpdatePlugins": "Oppdaterer programtillegg",
+ "TaskUpdatePlugins": "Oppdaterer programvaretillegg",
"TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.",
"TaskRefreshPeople": "Oppdater personar",
"TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.",
- "TaskCleanLogs": "Reins loggmappe",
+ "TaskCleanLogs": "Slett loggmappa",
"TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.",
"TaskRefreshLibrary": "Skann mediebibliotek",
"TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.",
"TaskRefreshChapterImages": "Trekk ut kapittelbilete",
- "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.",
- "TaskCleanCache": "Rens mappe for hurtiglager",
+ "TaskCleanCacheDescription": "Sletter mellomlagra filer som ikkje lengre trengst av systemet.",
+ "TaskCleanCache": "Fjern hurtigbuffer",
"TasksChannelsCategory": "Internettkanalar",
"TasksApplicationCategory": "Applikasjon",
"TasksLibraryCategory": "Bibliotek",
@@ -96,9 +96,9 @@
"TvShows": "TV-seriar",
"System": "System",
"SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}",
- "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.",
- "Songs": "Songar",
- "Shows": "Program",
+ "StartupEmbyServerIsLoading": "Jellyfin-tenaren laster. Prøv igjen seinare.",
+ "Songs": "Sangar",
+ "Shows": "Seriar",
"ServerNameNeedsToBeRestarted": "{0} må omstartast",
"ScheduledTaskStartedWithName": "{0} starta",
"ScheduledTaskFailedWithName": "{0} feila",
@@ -106,11 +106,16 @@
"PluginUpdatedWithName": "{0} blei oppdatert",
"PluginUninstalledWithName": "{0} blei avinstallert",
"PluginInstalledWithName": "{0} blei installert",
- "Plugin": "Programtillegg",
- "Playlists": "Speleliste",
- "Photos": "Foto",
+ "Plugin": "Programvaretillegg",
+ "Playlists": "Spelelister",
+ "Photos": "Bilete",
"NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa",
"NotificationOptionVideoPlayback": "Videoavspeling starta",
"NotificationOptionUserLockedOut": "Brukar er utestengd",
- "NotificationOptionTaskFailed": "Planlagt oppgåve feila"
+ "NotificationOptionTaskFailed": "Planlagt oppgåve feila",
+ "TaskCleanActivityLogDescription": "Sletter aktivitetslogginnlegg som er eldre enn den konfigurerte alderen.",
+ "TaskCleanActivityLog": "Slett aktivitetslogg",
+ "Undefined": "Udefinert",
+ "Forced": "Tvungen",
+ "Default": "Standard"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
index 469fa89b6..d1db09232 100644
--- a/Emby.Server.Implementations/Localization/Core/pa.json
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -1,5 +1,5 @@
{
- "TaskRefreshChapterImages": "ਐਬਸਟਰੈਕਟ ਅਧਿਆਇ ਅਧਿਆਇ",
+ "TaskRefreshChapterImages": "ਐਕਸਟਰੈਕਟ ਚੈਪਟਰ ਚਿੱਤਰ",
"TaskDownloadMissingSubtitlesDescription": "ਮੈਟਾਡੇਟਾ ਕੌਂਫਿਗਰੇਸ਼ਨ ਦੇ ਅਧਾਰ ਤੇ ਗਾਇਬ ਉਪਸਿਰਲੇਖਾਂ ਲਈ ਇੰਟਰਨੈਟ ਦੀ ਭਾਲ ਕਰਦਾ ਹੈ.",
"TaskDownloadMissingSubtitles": "ਗਾਇਬ ਉਪਸਿਰਲੇਖ ਡਾ Download ਨਲੋਡ ਕਰੋ",
"TaskRefreshChannelsDescription": "ਇੰਟਰਨੈੱਟ ਚੈਨਲ ਦੀ ਜਾਣਕਾਰੀ ਨੂੰ ਤਾਜ਼ਾ ਕਰਦਾ ਹੈ.",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 5ec8f1e88..323dcced0 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -16,7 +16,7 @@
"Folders": "Pastas",
"Genres": "Gêneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderContinueWatching": "Continuar Assistindo",
+ "HeaderContinueWatching": "Continuar assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episódios favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 46b47cf4a..e58f8c39d 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -39,7 +39,7 @@
"MixedContent": "Смешанное содержимое",
"Movies": "Кино",
"Music": "Музыка",
- "MusicVideos": "Музыкальные клипы",
+ "MusicVideos": "Муз. видео",
"NameInstallFailed": "Установка {0} неудачна",
"NameSeasonNumber": "Сезон {0}",
"NameSeasonUnknown": "Сезон неопознан",
@@ -75,7 +75,7 @@
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
- "Sync": "Синхронизация",
+ "Sync": "Синхро",
"System": "Система",
"TvShows": "ТВ",
"User": "Пользователь",
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index d785bcb90..15fb34186 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -4,11 +4,11 @@
"VersionNumber": "Верзија {0}",
"ValueSpecialEpisodeName": "Специјал - {0}",
"ValueHasBeenAddedToLibrary": "{0} је додато у вашу медијску библиотеку",
- "UserStoppedPlayingItemWithValues": "{0} заврши пуштање {1} на {2}",
+ "UserStoppedPlayingItemWithValues": "{0} завршио пуштање {1} на {2}",
"UserStartedPlayingItemWithValues": "{0} пушта {1} на {2}",
"UserPasswordChangedWithName": "Лозинка је промењена за корисника {0}",
"UserOnlineFromDevice": "{0} је на вези од {1}",
- "UserOfflineFromDevice": "{0} се одвезао са {1}",
+ "UserOfflineFromDevice": "{0} је прекинуо/а везу са {1}",
"UserLockedOutWithName": "Корисник {0} је закључан",
"UserDownloadingItemWithValues": "{0} преузима {1}",
"UserDeletedWithName": "Корисник {0} је обрисан",
@@ -41,7 +41,7 @@
"NotificationOptionPluginError": "Грешка прикључка",
"NotificationOptionNewLibraryContent": "Додат нови садржај",
"NotificationOptionInstallationFailed": "Неуспела инсталација",
- "NotificationOptionCameraImageUploaded": "Слика са камере послата",
+ "NotificationOptionCameraImageUploaded": "Слика са камере отпремљена",
"NotificationOptionAudioPlaybackStopped": "Заустављено пуштање звука",
"NotificationOptionAudioPlayback": "Покренуто пуштање звука",
"NotificationOptionApplicationUpdateInstalled": "Ажурирање инсталирано",
@@ -86,7 +86,7 @@
"Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
"Books": "Књиге",
- "AuthenticationSucceededWithUserName": "{0} успешно проверено",
+ "AuthenticationSucceededWithUserName": "{0} Успешна аутентикација",
"Artists": "Извођачи",
"Application": "Апликација",
"AppDeviceValues": "Апликација: {0}, Уређај: {1}",
@@ -100,7 +100,7 @@
"TaskUpdatePluginsDescription": "Преузима и инсталира исправке за додатке који су конфигурисани за аутоматско ажурирање.",
"TaskUpdatePlugins": "Ажурирајте додатке",
"TaskRefreshPeopleDescription": "Ажурира метаподатке за глумце и редитеље у вашој медијској библиотеци.",
- "TaskRefreshPeople": "Освежите људе",
+ "TaskRefreshPeople": "Освежите кориснике",
"TaskCleanLogsDescription": "Брише логове старије од {0} дана.",
"TaskCleanLogs": "Очистите директоријум логова",
"TaskRefreshLibraryDescription": "Скенира вашу медијску библиотеку за нове датотеке и освежава метаподатке.",
@@ -116,6 +116,6 @@
"TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
"TaskCleanActivityLog": "Очисти историју активности",
"Undefined": "Недефинисано",
- "Forced": "Форсирано",
+ "Forced": "Принудно",
"Default": "Подразумевано"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 345d41e9e..d992bf79b 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -39,7 +39,7 @@
"MixedContent": "Blandat innehåll",
"Movies": "Filmer",
"Music": "Musik",
- "MusicVideos": "Musikvideos",
+ "MusicVideos": "Musikvideor",
"NameInstallFailed": "{0} installationen misslyckades",
"NameSeasonNumber": "Säsong {0}",
"NameSeasonUnknown": "Okänd säsong",
@@ -117,5 +117,6 @@
"TaskCleanActivityLogDescription": "Radera aktivitets logg inlägg som är äldre än definerad ålder.",
"TaskCleanActivityLog": "Rensa Aktivitets Logg",
"Undefined": "odefinierad",
- "Forced": "Tvingad"
+ "Forced": "Tvingad",
+ "Default": "Standard"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index c737ba42b..129986ed0 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -69,7 +69,7 @@
"NameSeasonUnknown": "அறியப்படாத பருவம்",
"NameSeasonNumber": "பருவம் {0}",
"NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
- "MusicVideos": "இசைப்படங்கள்",
+ "MusicVideos": "இசை கானொளி",
"Music": "இசை",
"Movies": "திரைப்படங்கள்",
"Latest": "புதியவை",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 71dd2c7a3..e26010423 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -50,7 +50,7 @@
"HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
"HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
"HeaderContinueWatching": "ดูต่อ",
- "HeaderAlbumArtists": "อัลบั้มศิลปิน",
+ "HeaderAlbumArtists": "ศิลปินอัลบั้ม",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
"Favorites": "รายการโปรด",
@@ -112,5 +112,10 @@
"System": "ระบบ",
"Sync": "ซิงค์",
"SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
- "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
+ "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่",
+ "Default": "ค่าเริ่มต้น",
+ "TaskCleanActivityLogDescription": "ลบบันทึกกิจกรรมที่เก่ากว่าค่าที่กำหนดไว้",
+ "TaskCleanActivityLog": "ล้างบันทึกกิจกรรม",
+ "Undefined": "ไม่ได้กำหนด",
+ "Forced": "บังคับใช้"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index b6073bf6a..5a2069df5 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -1,5 +1,5 @@
{
- "MusicVideos": "Музичні кліпи",
+ "MusicVideos": "Музичні відеокліпи",
"Music": "Музика",
"Movies": "Фільми",
"MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
@@ -113,7 +113,7 @@
"MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
"Inherit": "Успадкувати",
"HeaderRecordingGroups": "Групи запису",
- "Forced": "Примусово",
+ "Forced": "Форсовані",
"TaskCleanActivityLogDescription": "Видаляє старші за встановлений термін записи з журналу активності.",
"TaskCleanActivityLog": "Очистити журнал активності",
"Undefined": "Не визначено",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 40368d464..58652c469 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -3,7 +3,7 @@
"Favorites": "Yêu Thích",
"Folders": "Thư Mục",
"Genres": "Thể Loại",
- "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
+ "HeaderAlbumArtists": "Tuyển Tập Nghệ sĩ",
"HeaderContinueWatching": "Xem Tiếp",
"HeaderLiveTV": "TV Trực Tiếp",
"Movies": "Phim",
@@ -13,7 +13,7 @@
"Songs": "Các Bài Hát",
"Sync": "Đồng Bộ",
"ValueSpecialEpisodeName": "Đặc Biệt - {0}",
- "Albums": "Albums",
+ "Albums": "Tuyển Tập",
"Artists": "Các Nghệ Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index affb0e099..c3b223f63 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -37,7 +37,7 @@
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
- "MusicVideos": "音樂MV",
+ "MusicVideos": "音樂錄影帶",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 3f9e22106..b1ff28c2c 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -1,17 +1,18 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Localization
@@ -36,7 +37,7 @@ namespace Emby.Server.Implementations.Localization
private List<CultureDto> _cultures;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationManager" /> class.
@@ -73,8 +74,7 @@ namespace Emby.Server.Implementations.Localization
using (var str = _assembly.GetManifestResourceStream(resource))
using (var reader = new StreamReader(str))
{
- string line;
- while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
if (string.IsNullOrWhiteSpace(line))
{
@@ -119,10 +119,8 @@ namespace Emby.Server.Implementations.Localization
using (var stream = _assembly.GetManifestResourceStream(ResourcePath))
using (var reader = new StreamReader(stream))
{
- while (!reader.EndOfStream)
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
if (string.IsNullOrWhiteSpace(line))
{
continue;
@@ -170,17 +168,27 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public CultureDto FindLanguageInfo(string language)
- => GetCultures()
- .FirstOrDefault(i =>
- string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase)
- || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase)
- || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase)
- || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
+ {
+ // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
+ for (var i = 0; i < _cultures.Count; i++)
+ {
+ var culture = _cultures[i];
+ if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
+ || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
+ || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
+ || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
+ {
+ return culture;
+ }
+ }
+
+ return default;
+ }
/// <inheritdoc />
public IEnumerable<CountryInfo> GetCountries()
{
- StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
+ using StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions);
}
@@ -225,7 +233,7 @@ namespace Emby.Server.Implementations.Localization
throw new ArgumentNullException(nameof(rating));
}
- if (_unratedValues.Contains(rating, StringComparer.OrdinalIgnoreCase))
+ if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -253,11 +261,11 @@ namespace Emby.Server.Implementations.Localization
var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1)
{
- rating = rating.Substring(index).TrimStart(':').Trim();
+ var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim();
- if (!string.IsNullOrWhiteSpace(rating))
+ if (!trimmedRating.IsEmpty)
{
- return GetRatingLevel(rating);
+ return GetRatingLevel(trimmedRating.ToString());
}
}
@@ -316,11 +324,11 @@ namespace Emby.Server.Implementations.Localization
}
const string Prefix = "Core";
- var key = Prefix + culture;
return _dictionaries.GetOrAdd(
- key,
- f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
+ culture,
+ (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
+ this);
}
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index f27305cbe..8aaa1f7bb 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -15,7 +17,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.MediaEncoder
@@ -82,11 +83,6 @@ namespace Emby.Server.Implementations.MediaEncoder
return false;
}
- if (video.VideoType == VideoType.Dvd)
- {
- return false;
- }
-
if (video.IsShortcut)
{
return false;
@@ -166,7 +162,7 @@ namespace Emby.Server.Implementations.MediaEncoder
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(",", video.Path));
+ _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
success = false;
break;
}
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 0781a0e33..137728616 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index 4e25768cf..a8b18d292 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 932f721ab..9a1ca9946 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -215,7 +217,7 @@ namespace Emby.Server.Implementations.Playlists
// Create a list of the new linked children to add to the playlist
var childrenToAdd = newItems
- .Select(i => LinkedChild.Create(i))
+ .Select(LinkedChild.Create)
.ToList();
// Log duplicates that have been ignored, if any
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index c26ccfd88..8fd61f2bc 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,8 +1,9 @@
-#nullable enable
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
@@ -11,9 +12,11 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Json.Converters;
+using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -30,9 +33,19 @@ namespace Emby.Server.Implementations.Plugins
private readonly ILogger<PluginManager> _logger;
private readonly IApplicationHost _appHost;
private readonly ServerConfiguration _config;
- private readonly IList<LocalPlugin> _plugins;
+ private readonly List<LocalPlugin> _plugins;
private readonly Version _minimumVersion;
+ private IHttpClientFactory? _httpClientFactory;
+
+ private IHttpClientFactory HttpClientFactory
+ {
+ get
+ {
+ return _httpClientFactory ?? (_httpClientFactory = _appHost.Resolve<IHttpClientFactory>());
+ }
+ }
+
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
@@ -51,7 +64,7 @@ namespace Emby.Server.Implementations.Plugins
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_pluginsPath = pluginsPath;
_appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
- _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options)
{
WriteIndented = true
};
@@ -75,7 +88,7 @@ namespace Emby.Server.Implementations.Plugins
/// <summary>
/// Gets the Plugins.
/// </summary>
- public IList<LocalPlugin> Plugins => _plugins;
+ public IReadOnlyList<LocalPlugin> Plugins => _plugins;
/// <summary>
/// Returns all the assemblies.
@@ -146,9 +159,7 @@ namespace Emby.Server.Implementations.Plugins
/// </summary>
public void CreatePlugins()
{
- _ = _appHost.GetExports<IPlugin>(CreatePluginInstance)
- .Where(i => i != null)
- .ToArray();
+ _ = _appHost.GetExports<IPlugin>(CreatePluginInstance);
}
/// <summary>
@@ -258,11 +269,7 @@ namespace Emby.Server.Implementations.Plugins
// If no version is given, return the current instance.
var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList();
- plugin = plugins.FirstOrDefault(p => p.Instance != null);
- if (plugin == null)
- {
- plugin = plugins.OrderByDescending(p => p.Version).FirstOrDefault();
- }
+ plugin = plugins.FirstOrDefault(p => p.Instance != null) ?? plugins.OrderByDescending(p => p.Version).FirstOrDefault();
}
else
{
@@ -332,34 +339,76 @@ namespace Emby.Server.Implementations.Plugins
ChangePluginState(plugin, PluginStatus.Malfunctioned);
}
- /// <summary>
- /// Saves the manifest back to disk.
- /// </summary>
- /// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
- /// <param name="path">The path where to save the manifest.</param>
- /// <returns>True if successful.</returns>
+ /// <inheritdoc/>
public bool SaveManifest(PluginManifest manifest, string path)
{
- if (manifest == null)
- {
- return false;
- }
-
try
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
File.WriteAllText(Path.Combine(path, "meta.json"), data);
return true;
}
-#pragma warning disable CA1031 // Do not catch general exception types
- catch (Exception e)
-#pragma warning restore CA1031 // Do not catch general exception types
+ catch (ArgumentException e)
{
- _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
+ _logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
return false;
}
}
+ /// <inheritdoc/>
+ public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
+ {
+ if (packageInfo == null)
+ {
+ return false;
+ }
+
+ var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
+ var imagePath = string.Empty;
+
+ if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
+ {
+ var url = new Uri(packageInfo.ImageUrl);
+ imagePath = Path.Join(path, url.Segments[^1]);
+
+ await using var fileStream = File.OpenWrite(imagePath);
+
+ try
+ {
+ await using var downloadStream = await HttpClientFactory
+ .CreateClient(NamedClient.Default)
+ .GetStreamAsync(url)
+ .ConfigureAwait(false);
+
+ await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+ catch (HttpRequestException ex)
+ {
+ _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
+ imagePath = string.Empty;
+ }
+ }
+
+ var manifest = new PluginManifest
+ {
+ Category = packageInfo.Category,
+ Changelog = versionInfo.Changelog ?? string.Empty,
+ Description = packageInfo.Description,
+ Id = packageInfo.Id,
+ Name = packageInfo.Name,
+ Overview = packageInfo.Overview,
+ Owner = packageInfo.Owner,
+ TargetAbi = versionInfo.TargetAbi ?? string.Empty,
+ Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
+ Version = versionInfo.Version,
+ Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
+ AutoUpdate = true,
+ ImagePath = imagePath
+ };
+
+ return SaveManifest(manifest, path);
+ }
+
/// <summary>
/// Changes a plugin's load status.
/// </summary>
@@ -406,11 +455,12 @@ namespace Emby.Server.Implementations.Plugins
try
{
_logger.LogDebug("Creating instance of {Type}", type);
- var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
+ // _appHost.ServiceProvider is already assigned when we create the plugins
+ var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider!, type);
if (plugin == null)
{
// Create a dummy record for the providers.
- // TODO: remove this code, if all provided have been released as separate plugins.
+ // TODO: remove this code once all provided have been released as separate plugins.
plugin = new LocalPlugin(
instance.AssemblyFilePath,
true,
@@ -617,7 +667,7 @@ namespace Emby.Server.Implementations.Plugins
var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{
- entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
+ entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories);
if (entry.IsEnabledAndSupported)
{
lastName = entry.Name;
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 7bed06de3..7cfd1fced 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -1,9 +1,10 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
-using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
@@ -258,20 +259,17 @@ namespace Emby.Server.Implementations.QuickConnect
}
// Expire stale connection requests
- var code = string.Empty;
- var values = _currentRequests.Values.ToList();
-
- for (int i = 0; i < values.Count; i++)
+ foreach (var (_, currentRequest) in _currentRequests)
{
- var added = values[i].DateAdded ?? DateTime.UnixEpoch;
- if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
+ var added = currentRequest.DateAdded ?? DateTime.UnixEpoch;
+ if (expireAll || DateTime.UtcNow > added.AddMinutes(Timeout))
{
- code = values[i].Code;
- _logger.LogDebug("Removing expired request {code}", code);
+ var code = currentRequest.Code;
+ _logger.LogDebug("Removing expired request {Code}", code);
if (!_currentRequests.TryRemove(code, out _))
{
- _logger.LogWarning("Request {code} already expired", code);
+ _logger.LogWarning("Request {Code} already expired", code);
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index b302303f8..d7e320754 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -1,10 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -69,7 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// The options for the json Serializer.
/// </summary>
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
@@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
lock (_lastExecutionResultSyncLock)
{
using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
- JsonSerializer.SerializeAsync(createStream, value, _jsonOptions);
+ using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream);
+ JsonSerializer.Serialize(jsonStream, value, _jsonOptions);
}
}
}
@@ -301,12 +303,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
get
{
- if (_id == null)
- {
- _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
-
- return _id;
+ return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
}
@@ -348,9 +345,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
var trigger = (ITaskTrigger)sender;
- var configurableTask = ScheduledTask as IConfigurableScheduledTask;
-
- if (configurableTask != null && !configurableTask.IsEnabled)
+ if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled)
{
return;
}
@@ -578,7 +573,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
Directory.CreateDirectory(Path.GetDirectoryName(path));
using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
- JsonSerializer.SerializeAsync(createStream, triggers, _jsonOptions);
+ using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream);
+ JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions);
}
/// <summary>
@@ -715,11 +711,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
}
- return new DailyTrigger
- {
- TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
- TaskOptions = options
- };
+ return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
}
if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
@@ -734,12 +726,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));
}
- return new WeeklyTrigger
- {
- TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
- DayOfWeek = info.DayOfWeek.Value,
- TaskOptions = options
- };
+ return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
}
if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
@@ -749,16 +736,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));
}
- return new IntervalTrigger
- {
- Interval = TimeSpan.FromTicks(info.IntervalTicks.Value),
- TaskOptions = options
- };
+ return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
}
if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
{
- return new StartupTrigger();
+ return new StartupTrigger(options);
}
throw new ArgumentException("Unrecognized trigger type: " + info.Type);
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index af316e108..4f0df75bf 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 171e44258..baeb86a22 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -12,9 +12,9 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
{
@@ -140,10 +140,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
previouslyFailedImages.Add(key);
var parentPath = Path.GetDirectoryName(failHistoryPath);
+ if (parentPath != null)
+ {
+ Directory.CreateDirectory(parentPath);
+ }
- Directory.CreateDirectory(parentPath);
-
- string text = string.Join("|", previouslyFailedImages);
+ string text = string.Join('|', previouslyFailedImages);
File.WriteAllText(failHistoryPath, text);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
index 4abbf784b..50ba9bc89 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -75,4 +75,4 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return Enumerable.Empty<TaskTriggerInfo>();
}
}
-} \ No newline at end of file
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
new file mode 100644
index 000000000..1ad1d0f50
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Server.Implementations;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Optimizes Jellyfin's database by issuing a VACUUM command.
+ /// </summary>
+ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ private readonly ILogger<OptimizeDatabaseTask> _logger;
+ private readonly ILocalizationManager _localization;
+ private readonly JellyfinDbProvider _provider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
+ /// </summary>
+ public OptimizeDatabaseTask(
+ ILogger<OptimizeDatabaseTask> logger,
+ ILocalizationManager localization,
+ JellyfinDbProvider provider)
+ {
+ _logger = logger;
+ _localization = localization;
+ _provider = provider;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public string Key => "OptimizeDatabaseTask";
+
+ /// <inheritdoc />
+ public bool IsHidden => false;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[]
+ {
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ _logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
+
+ try
+ {
+ using var context = _provider.CreateContext();
+ if (context.Database.IsSqlite())
+ {
+ context.Database.ExecuteSqlRaw("PRAGMA optimize");
+ context.Database.ExecuteSqlRaw("VACUUM");
+ _logger.LogInformation("jellyfin.db optimized successfully!");
+ }
+ else
+ {
+ _logger.LogInformation("This database doesn't support optimization");
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error while optimizing jellyfin.db");
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index c384cf4bb..57d294a40 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -5,8 +5,8 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index a69380cbb..11a5fb79f 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -9,7 +9,6 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index e470adcf4..51b620404 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -6,8 +6,8 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
index 3b40320ab..29ab6a73d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
@@ -8,29 +8,31 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that fires everyday.
/// </summary>
- public class DailyTrigger : ITaskTrigger
+ public sealed class DailyTrigger : ITaskTrigger
{
- /// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
+ private readonly TimeSpan _timeOfDay;
+ private Timer? _timer;
/// <summary>
- /// Gets or sets the time of day to trigger the task to run.
+ /// Initializes a new instance of the <see cref="DailyTrigger"/> class.
/// </summary>
- /// <value>The time of day.</value>
- public TimeSpan TimeOfDay { get; set; }
+ /// <param name="timeofDay">The time of day to trigger the task to run.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public DailyTrigger(TimeSpan timeofDay, TaskOptions taskOptions)
+ {
+ _timeOfDay = timeofDay;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -45,14 +47,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
var now = DateTime.Now;
- var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date;
- triggerDate = triggerDate.Add(TimeOfDay);
+ var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date;
+ triggerDate = triggerDate.Add(_timeOfDay);
var dueTime = triggerDate - now;
logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime);
- Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -68,10 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
index b04fd7c7e..30568e809 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
@@ -9,31 +9,32 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that runs repeatedly on an interval.
/// </summary>
- public class IntervalTrigger : ITaskTrigger
+ public sealed class IntervalTrigger : ITaskTrigger
{
+ private readonly TimeSpan _interval;
private DateTime _lastStartDate;
+ private Timer? _timer;
/// <summary>
- /// Occurs when [triggered].
+ /// Initializes a new instance of the <see cref="IntervalTrigger"/> class.
/// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
- /// Gets or sets the interval.
- /// </summary>
- /// <value>The interval.</value>
- public TimeSpan Interval { get; set; }
+ /// <param name="interval">The interval.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions)
+ {
+ _interval = interval;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -55,7 +56,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
else
{
- triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(Interval);
+ triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(_interval);
}
if (DateTime.UtcNow > triggerDate)
@@ -71,7 +72,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
dueTime = maxDueTime;
}
- Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -87,10 +88,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
index 7cd5493da..18b9a8b75 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
@@ -10,24 +10,28 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Class StartupTaskTrigger.
/// </summary>
- public class StartupTrigger : ITaskTrigger
+ public sealed class StartupTrigger : ITaskTrigger
{
+ public const int DelayMs = 3000;
+
/// <summary>
- /// Occurs when [triggered].
+ /// Initializes a new instance of the <see cref="StartupTrigger"/> class.
/// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- public int DelayMs { get; set; }
+ /// <param name="taskOptions">The options of this task.</param>
+ public StartupTrigger(TaskOptions taskOptions)
+ {
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
- public StartupTrigger()
- {
- DelayMs = 3000;
- }
+ /// <summary>
+ /// Gets the options of this task.
+ /// </summary>
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
index 0c0ebec08..36ae190b0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
@@ -8,35 +8,34 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that fires on a weekly basis.
/// </summary>
- public class WeeklyTrigger : ITaskTrigger
+ public sealed class WeeklyTrigger : ITaskTrigger
{
- /// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
- /// Gets or sets the time of day to trigger the task to run.
- /// </summary>
- /// <value>The time of day.</value>
- public TimeSpan TimeOfDay { get; set; }
+ private readonly TimeSpan _timeOfDay;
+ private readonly DayOfWeek _dayOfWeek;
+ private Timer? _timer;
/// <summary>
- /// Gets or sets the day of week.
+ /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class.
/// </summary>
- /// <value>The day of week.</value>
- public DayOfWeek DayOfWeek { get; set; }
+ /// <param name="timeofDay">The time of day to trigger the task to run.</param>
+ /// <param name="dayOfWeek">The day of week.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public WeeklyTrigger(TimeSpan timeofDay, DayOfWeek dayOfWeek, TaskOptions taskOptions)
+ {
+ _timeOfDay = timeofDay;
+ _dayOfWeek = dayOfWeek;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -51,7 +50,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
var triggerDate = GetNextTriggerDateTime();
- Timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -63,22 +62,22 @@ namespace Emby.Server.Implementations.ScheduledTasks
var now = DateTime.Now;
// If it's on the same day
- if (now.DayOfWeek == DayOfWeek)
+ if (now.DayOfWeek == _dayOfWeek)
{
// It's either later today, or a week from now
- return now.TimeOfDay < TimeOfDay ? now.Date.Add(TimeOfDay) : now.Date.AddDays(7).Add(TimeOfDay);
+ return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay);
}
var triggerDate = now.Date;
// Walk the date forward until we get to the trigger day
- while (triggerDate.DayOfWeek != DayOfWeek)
+ while (triggerDate.DayOfWeek != _dayOfWeek)
{
triggerDate = triggerDate.AddDays(1);
}
// Return the trigger date plus the time offset
- return triggerDate.Add(TimeOfDay);
+ return triggerDate.Add(_timeOfDay);
}
/// <summary>
@@ -94,10 +93,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
index 4bc12f44a..e8eac315b 100644
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -289,7 +291,7 @@ namespace Emby.Server.Implementations.Security
return result;
}
- private static AuthenticationInfo Get(IReadOnlyList<IResultSetValue> reader)
+ private static AuthenticationInfo Get(IReadOnlyList<ResultSetValue> reader)
{
var info = new AuthenticationInfo
{
@@ -297,50 +299,49 @@ namespace Emby.Server.Implementations.Security
AccessToken = reader[1].ToString()
};
- if (reader[2].SQLiteType != SQLiteType.Null)
- {
- info.DeviceId = reader[2].ToString();
- }
-
- if (reader[3].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(2, out var deviceId))
{
- info.AppName = reader[3].ToString();
+ info.DeviceId = deviceId;
}
- if (reader[4].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(3, out var appName))
{
- info.AppVersion = reader[4].ToString();
+ info.AppName = appName;
}
- if (reader[5].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(4, out var appVersion))
{
- info.DeviceName = reader[5].ToString();
+ info.AppVersion = appVersion;
}
- if (reader[6].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(6, out var userId))
{
- info.UserId = new Guid(reader[6].ToString());
+ info.UserId = new Guid(userId);
}
- if (reader[7].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(7, out var userName))
{
- info.UserName = reader[7].ToString();
+ info.UserName = userName;
}
info.DateCreated = reader[8].ReadDateTime();
- if (reader[9].SQLiteType != SQLiteType.Null)
+ if (reader.TryReadDateTime(9, out var dateLastActivity))
{
- info.DateLastActivity = reader[9].ReadDateTime();
+ info.DateLastActivity = dateLastActivity;
}
else
{
info.DateLastActivity = info.DateCreated;
}
- if (reader[10].SQLiteType != SQLiteType.Null)
+ if (reader.TryGetString(10, out var customName))
+ {
+ info.DeviceName = customName;
+ }
+ else if (reader.TryGetString(5, out var deviceName))
{
- info.DeviceName = reader[10].ToString();
+ info.DeviceName = deviceName;
}
return info;
@@ -361,9 +362,9 @@ namespace Emby.Server.Implementations.Security
foreach (var row in statement.ExecuteQuery())
{
- if (row[0].SQLiteType != SQLiteType.Null)
+ if (row.TryGetString(0, out var customName))
{
- result.CustomName = row[0].ToString();
+ result.CustomName = customName;
}
}
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 27024e4e1..5ff73de81 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -19,7 +19,10 @@ namespace Emby.Server.Implementations.Serialization
new ConcurrentDictionary<string, XmlSerializer>();
private static XmlSerializer GetSerializer(Type type)
- => _serializers.GetOrAdd(type.FullName, _ => new XmlSerializer(type));
+ => _serializers.GetOrAdd(
+ type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
+ (_, t) => new XmlSerializer(t),
+ type);
/// <summary>
/// Serializes to writer.
@@ -38,7 +41,7 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="type">The type.</param>
/// <param name="stream">The stream.</param>
/// <returns>System.Object.</returns>
- public object DeserializeFromStream(Type type, Stream stream)
+ public object? DeserializeFromStream(Type type, Stream stream)
{
using (var reader = XmlReader.Create(stream))
{
@@ -81,7 +84,7 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="type">The type.</param>
/// <param name="file">The file.</param>
/// <returns>System.Object.</returns>
- public object DeserializeFromFile(Type type, string file)
+ public object? DeserializeFromFile(Type type, string file)
{
using (var stream = File.OpenRead(file))
{
@@ -95,7 +98,7 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="type">The type.</param>
/// <param name="buffer">The buffer.</param>
/// <returns>System.Object.</returns>
- public object DeserializeFromBytes(Type type, byte[] buffer)
+ public object? DeserializeFromBytes(Type type, byte[] buffer)
{
using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true))
{
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index ac589b03c..6cf9a8f71 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -25,6 +25,10 @@ namespace Emby.Server.Implementations
cacheDirectoryPath,
webDirectoryPath)
{
+ // ProgramDataPath cannot change when the server is running, so cache these to avoid allocations.
+ RootFolderPath = Path.Join(ProgramDataPath, "root");
+ DefaultUserViewsPath = Path.Combine(RootFolderPath, "default");
+ DefaultInternalMetadataPath = Path.Combine(ProgramDataPath, "metadata");
InternalMetadataPath = DefaultInternalMetadataPath;
}
@@ -32,13 +36,13 @@ namespace Emby.Server.Implementations
/// Gets the path to the base root media directory.
/// </summary>
/// <value>The root folder path.</value>
- public string RootFolderPath => Path.Combine(ProgramDataPath, "root");
+ public string RootFolderPath { get; }
/// <summary>
/// Gets the path to the default user view directory. Used if no specific user view is defined.
/// </summary>
/// <value>The default user views path.</value>
- public string DefaultUserViewsPath => Path.Combine(RootFolderPath, "default");
+ public string DefaultUserViewsPath { get; }
/// <summary>
/// Gets the path to the People directory.
@@ -98,7 +102,7 @@ namespace Emby.Server.Implementations
public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users");
/// <inheritdoc/>
- public string DefaultInternalMetadataPath => Path.Combine(ProgramDataPath, "metadata");
+ public string DefaultInternalMetadataPath { get; }
/// <inheritdoc />
public string InternalMetadataPath { get; set; }
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 10e28c33a..62df354fd 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -1475,17 +1477,14 @@ namespace Emby.Server.Implementations.Session
user = _userManager.GetUserById(request.UserId);
}
- if (user == null)
- {
- user = _userManager.GetUserByName(request.Username);
- }
+ user ??= _userManager.GetUserByName(request.Username);
if (enforcePassword)
{
user = await _userManager.AuthenticateUser(
request.Username,
request.Password,
- request.PasswordSha1,
+ null,
request.RemoteEndPoint,
true).ConfigureAwait(false);
}
@@ -1543,23 +1542,26 @@ namespace Emby.Server.Implementations.Session
Limit = 1
}).Items.FirstOrDefault();
- var allExistingForDevice = _authRepo.Get(
- new AuthenticationInfoQuery
- {
- DeviceId = deviceId
- }).Items;
-
- foreach (var auth in allExistingForDevice)
+ if (!string.IsNullOrEmpty(deviceId))
{
- if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
- {
- try
+ var allExistingForDevice = _authRepo.Get(
+ new AuthenticationInfoQuery
{
- Logout(auth);
- }
- catch (Exception ex)
+ DeviceId = deviceId
+ }).Items;
+
+ foreach (var auth in allExistingForDevice)
+ {
+ if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
{
- _logger.LogError(ex, "Error while logging out.");
+ try
+ {
+ Logout(auth);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while logging out.");
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 39c369a01..e9e3ca7f4 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index f9c6a13c6..9fa92a53a 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,6 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1600
-#nullable enable
using System;
using System.Collections.Generic;
@@ -8,7 +6,6 @@ using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 1f68a9c81..2b0ab536f 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
@@ -131,11 +131,11 @@ namespace Emby.Server.Implementations.Sorting
return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y));
}
- private static int GetSpecialCompareValue(Episode item)
+ private static long GetSpecialCompareValue(Episode item)
{
// First sort by season number
// Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough)
- var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000;
+ var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000L;
// Second sort order is if it airs after the season
if (item.AirsAfterSeasonNumber.HasValue)
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 7657cc74e..42e644970 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
var audio = x as IHasAlbumArtist;
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 7dfdd9ecf..1db3f5e9c 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
var audio = x as Audio;
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
index 756d3c5b6..98bee3fd9 100644
--- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
public string Name => ItemSortBy.Artist;
/// <inheritdoc />
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
if (!(x is Audio audio))
{
diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
index 980954ba0..5f142fa4b 100644
--- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index fa136c36d..d20dedc2d 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -15,14 +15,14 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
- private static float GetValue(BaseItem x)
+ private static float GetValue(BaseItem? x)
{
- return x.CriticRating ?? 0;
+ return x?.CriticRating ?? 0;
}
/// <summary>
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index cbca300d2..d3f10f78c 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index 03ff19d21..b1cb123ce 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 16bd2aff8..08a44319f 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index 0c4e82d01..73e628cf7 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
index a35192eff..3c5ddeefa 100644
--- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
@@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
@@ -30,9 +30,9 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
- private static int GetValue(BaseItem x)
+ private static int GetValue(BaseItem? x)
{
- return x.IsFolder ? 0 : 1;
+ return x?.IsFolder ?? true ? 0 : 1;
}
}
}
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index d95948406..7d77a8bc5 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index 1632c5a7a..926835f90 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index da020d8d8..4de81a69e 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index 76bb798b5..a81f78ebf 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 5c2830322..04e4865cb 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index 92ac04dc6..c98f97bf1 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetDate(x).CompareTo(GetDate(y));
}
@@ -26,8 +26,13 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>DateTime.</returns>
- private static DateTime GetDate(BaseItem x)
+ private static DateTime GetDate(BaseItem? x)
{
+ if (x == null)
+ {
+ return DateTime.MinValue;
+ }
+
if (x.PremiereDate.HasValue)
{
return x.PremiereDate.Value;
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index e2857df0b..df9f9957d 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
@@ -25,8 +25,13 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>DateTime.</returns>
- private static int GetValue(BaseItem x)
+ private static int GetValue(BaseItem? x)
{
+ if (x == null)
+ {
+ return 0;
+ }
+
if (x.ProductionYear.HasValue)
{
return x.ProductionYear.Value;
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index 7739d0418..af3bc2750 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return Guid.NewGuid().CompareTo(Guid.NewGuid());
}
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index dde44333d..129315303 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index b9205ee07..4123a59f8 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index f745e193b..8d30716d3 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index 558a3d351..c3df7c47e 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 5766dc542..01445c525 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs
index 7c2ad2477..12efff261 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index aee959c53..993456196 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -87,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
_sessionManager = sessionManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
- _sessionManager.SessionControllerConnected += OnSessionControllerConnected;
+ _sessionManager.SessionEnded += OnSessionEnded;
}
/// <inheritdoc />
@@ -269,14 +271,17 @@ namespace Emby.Server.Implementations.SyncPlay
var user = _userManager.GetUserById(session.UserId);
List<GroupInfoDto> list = new List<GroupInfoDto>();
- foreach (var group in _groups.Values)
+ lock (_groupsLock)
{
- // Locking required as group is not thread-safe.
- lock (group)
+ foreach (var (_, group) in _groups)
{
- if (group.HasAccessToPlayQueue(user))
+ // Locking required as group is not thread-safe.
+ lock (group)
{
- list.Add(group.GetInfo());
+ if (group.HasAccessToPlayQueue(user))
+ {
+ list.Add(group.GetInfo());
+ }
}
}
}
@@ -352,18 +357,18 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
- _sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
+ _sessionManager.SessionEnded -= OnSessionEnded;
_disposed = true;
}
- private void OnSessionControllerConnected(object sender, SessionEventArgs e)
+ private void OnSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
{
- var request = new JoinGroupRequest(group.GroupId);
- JoinGroup(session, request, CancellationToken.None);
+ var leaveGroupRequest = new LeaveGroupRequest();
+ LeaveGroup(session, leaveGroupRequest, CancellationToken.None);
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 839b62448..af453d148 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -1,8 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -11,7 +12,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Series = MediaBrowser.Controller.Entities.TV.Series;
@@ -23,12 +23,14 @@ namespace Emby.Server.Implementations.TV
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _configurationManager;
- public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager)
+ public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager)
{
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
+ _configurationManager = configurationManager;
}
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
@@ -43,9 +45,7 @@ namespace Emby.Server.Implementations.TV
string presentationUniqueKey = null;
if (!string.IsNullOrEmpty(request.SeriesId))
{
- var series = _libraryManager.GetItemById(request.SeriesId) as Series;
-
- if (series != null)
+ if (_libraryManager.GetItemById(request.SeriesId) is Series series)
{
presentationUniqueKey = GetUniqueSeriesKey(series);
}
@@ -95,9 +95,7 @@ namespace Emby.Server.Implementations.TV
int? limit = null;
if (!string.IsNullOrEmpty(request.SeriesId))
{
- var series = _libraryManager.GetItemById(request.SeriesId) as Series;
-
- if (series != null)
+ if (_libraryManager.GetItemById(request.SeriesId) is Series series)
{
presentationUniqueKey = GetUniqueSeriesKey(series);
limit = 1;
@@ -156,7 +154,7 @@ namespace Emby.Server.Implementations.TV
return i.Item1 != DateTime.MinValue;
}
- if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
+ if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff))
{
anyFound = true;
return true;
@@ -200,13 +198,10 @@ namespace Emby.Server.Implementations.TV
ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions
{
- Fields = new ItemFields[]
- {
- ItemFields.SortName
- },
+ Fields = new[] { ItemFields.SortName },
EnableImages = false
}
- }).FirstOrDefault();
+ }).Cast<Episode>().FirstOrDefault();
Func<Episode> getEpisode = () =>
{
@@ -224,6 +219,43 @@ namespace Emby.Server.Implementations.TV
DtoOptions = dtoOptions
}).Cast<Episode>().FirstOrDefault();
+ if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
+ {
+ var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ AncestorWithPresentationUniqueKey = null,
+ SeriesPresentationUniqueKey = seriesKey,
+ ParentIndexNumber = 0,
+ IncludeItemTypes = new[] { nameof(Episode) },
+ IsPlayed = false,
+ IsVirtualItem = false,
+ DtoOptions = dtoOptions
+ })
+ .Cast<Episode>()
+ .Where(episode => episode.AirsBeforeSeasonNumber != null || episode.AirsAfterSeasonNumber != null)
+ .ToList();
+
+ if (lastWatchedEpisode != null)
+ {
+ // Last watched episode is added, because there could be specials that aired before the last watched episode
+ consideredEpisodes.Add(lastWatchedEpisode);
+ }
+
+ if (nextEpisode != null)
+ {
+ consideredEpisodes.Add(nextEpisode);
+ }
+
+ var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
+ .Cast<Episode>();
+ if (lastWatchedEpisode != null)
+ {
+ sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1);
+ }
+
+ nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
+ }
+
if (nextEpisode != null)
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 4fd7ac0c1..8179e26c5 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -18,17 +18,17 @@ namespace Emby.Server.Implementations.Udp
public sealed class UdpServer : IDisposable
{
/// <summary>
+ /// Address Override Configuration Key.
+ /// </summary>
+ public const string AddressOverrideConfigKey = "PublishedServerUrl";
+
+ /// <summary>
/// The _logger.
/// </summary>
private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
- /// <summary>
- /// Address Override Configuration Key.
- /// </summary>
- public const string AddressOverrideConfigKey = "PublishedServerUrl";
-
private Socket _udpSocket;
private IPEndPoint _endpoint;
private readonly byte[] _receiveBuffer = new byte[8192];
@@ -38,54 +38,58 @@ namespace Emby.Server.Implementations.Udp
/// <summary>
/// Initializes a new instance of the <see cref="UdpServer" /> class.
/// </summary>
- public UdpServer(ILogger logger, IServerApplicationHost appHost, IConfiguration configuration)
+ /// <param name="logger">The logger.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="configuration">The configuration manager.</param>
+ /// <param name="port">The port.</param>
+ public UdpServer(
+ ILogger logger,
+ IServerApplicationHost appHost,
+ IConfiguration configuration,
+ int port)
{
_logger = logger;
_appHost = appHost;
_config = configuration;
+
+ _endpoint = new IPEndPoint(IPAddress.Any, port);
+
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
{
- string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
- ? _config[AddressOverrideConfigKey]
- : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
+ string? localUrl = _config[AddressOverrideConfigKey];
+ if (string.IsNullOrEmpty(localUrl))
+ {
+ localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
+ }
- if (!string.IsNullOrEmpty(localUrl))
+ if (string.IsNullOrEmpty(localUrl))
{
- var response = new ServerDiscoveryInfo
- {
- Address = localUrl,
- Id = _appHost.SystemId,
- Name = _appHost.FriendlyName
- };
+ _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined.");
+ return;
+ }
- try
- {
- await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
- }
- catch (SocketException ex)
- {
- _logger.LogError(ex, "Error sending response message");
- }
+ var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+
+ try
+ {
+ await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
}
- else
+ catch (SocketException ex)
{
- _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined.");
+ _logger.LogError(ex, "Error sending response message");
}
}
/// <summary>
/// Starts the specified port.
/// </summary>
- /// <param name="port">The port.</param>
- /// <param name="cancellationToken"></param>
- public void Start(int port, CancellationToken cancellationToken)
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ public void Start(CancellationToken cancellationToken)
{
- _endpoint = new IPEndPoint(IPAddress.Any, port);
-
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
- _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_udpSocket.Bind(_endpoint);
_ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
@@ -93,9 +97,9 @@ namespace Emby.Server.Implementations.Udp
private async Task BeginReceiveAsync(CancellationToken cancellationToken)
{
+ var infiniteTask = Task.Delay(-1, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
- var infiniteTask = Task.Delay(-1, cancellationToken);
try
{
var task = _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index abcb4313f..b0921cbd8 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -22,6 +20,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
@@ -92,7 +91,7 @@ namespace Emby.Server.Implementations.Updates
_httpClientFactory = httpClientFactory;
_config = config;
_zipClient = zipClient;
- _jsonSerializerOptions = JsonDefaults.GetOptions();
+ _jsonSerializerOptions = JsonDefaults.Options;
_pluginManager = pluginManager;
}
@@ -100,12 +99,12 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
- public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
+ public async Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
- List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
+ PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetFromJsonAsync<PackageInfo[]>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
if (packages == null)
{
@@ -178,31 +177,20 @@ namespace Emby.Server.Implementations.Updates
// Where repositories have the same content, the details from the first is taken.
foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{
- if (!Guid.TryParse(package.Id, out var packageGuid))
- {
- // Package doesn't have a valid GUID, skip.
- continue;
- }
-
- var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
+ var existing = FilterPackages(result, package.Name, package.Id).FirstOrDefault();
// Remove invalid versions from the valid package.
for (var i = package.Versions.Count - 1; i >= 0; i--)
{
var version = package.Versions[i];
- var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
- // Update the manifests, if anything changes.
+ var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
if (plugin != null)
{
- if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
- {
- plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
- _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
- }
+ await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
}
- // Remove versions with a target abi that is greater then the current application version.
+ // Remove versions with a target ABI greater then the current application version.
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
{
package.Versions.RemoveAt(i);
@@ -235,7 +223,7 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? specificVersion = null)
{
if (name != null)
@@ -243,9 +231,9 @@ namespace Emby.Server.Implementations.Updates
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
- if (id != Guid.Empty)
+ if (id != default)
{
- availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id);
+ availablePackages = availablePackages.Where(x => x.Id == id);
}
if (specificVersion != null)
@@ -260,7 +248,7 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? minVersion = null,
Version? specificVersion = null)
{
@@ -290,11 +278,12 @@ namespace Emby.Server.Implementations.Updates
yield return new InstallationInfo
{
Changelog = v.Changelog,
- Id = new Guid(package.Id),
+ Id = package.Id,
Name = package.Name,
Version = v.VersionNumber,
SourceUrl = v.SourceUrl,
- Checksum = v.Checksum
+ Checksum = v.Checksum,
+ PackageInfo = package
};
}
}
@@ -504,7 +493,8 @@ namespace Emby.Server.Implementations.Updates
var plugins = _pluginManager.Plugins;
foreach (var plugin in plugins)
{
- if (plugin.Manifest?.AutoUpdate == false)
+ // Don't auto update when plugin marked not to, or when it's disabled.
+ if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
{
continue;
}
@@ -519,7 +509,7 @@ namespace Emby.Server.Implementations.Updates
}
}
- private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
+ private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
@@ -571,24 +561,16 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
+ await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir);
}
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
- // Set last update time if we were installed before
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
- if (plugin != null)
- {
- plugin.Manifest.Timestamp = DateTime.UtcNow;
- _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
- }
-
- // Do the install
- await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
- // Do plugin-specific processing
+ await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
return plugin != null;
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index 7d68aecf9..392498c53 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -77,8 +77,9 @@ namespace Jellyfin.Api.Auth
return false;
}
- var ip = _httpContextAccessor.HttpContext.GetNormalizedRemoteIp();
- var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
+ var isInLocalNetwork = _httpContextAccessor.HttpContext != null
+ && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
+
// User cannot access remotely and user is remote
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
{
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 4b2e5e7ea..154a56702 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -77,6 +77,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Total record count.</param>
/// <response code="200">Artists returned.</response>
@@ -112,6 +114,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -150,7 +154,8 @@ namespace Jellyfin.Api.Controllers
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
@@ -276,6 +281,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Total record count.</param>
/// <response code="200">Album artists returned.</response>
@@ -311,6 +318,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -349,7 +358,8 @@ namespace Jellyfin.Api.Controllers
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 616fe5b91..a6e70e72d 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -168,22 +168,22 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -333,22 +333,22 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index e1c9f69f6..b6309baab 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,3 +1,4 @@
+using System;
using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Text.Json;
@@ -25,7 +26,7 @@ namespace Jellyfin.Api.Controllers
private readonly IServerConfigurationManager _configurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
@@ -94,6 +95,11 @@ namespace Jellyfin.Api.Controllers
{
var configurationType = _configurationManager.GetConfigurationType(key);
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
+ if (configuration == null)
+ {
+ throw new ArgumentException("Body doesn't contain a valid configuration");
+ }
+
_configurationManager.SaveConfiguration(key, configuration);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index b2baa9cea..445733c24 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -6,7 +6,6 @@ using System.Net.Mime;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Http;
@@ -22,22 +21,18 @@ namespace Jellyfin.Api.Controllers
public class DashboardController : BaseJellyfinApiController
{
private readonly ILogger<DashboardController> _logger;
- private readonly IServerApplicationHost _appHost;
private readonly IPluginManager _pluginManager;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> class.
/// </summary>
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
- /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
public DashboardController(
ILogger<DashboardController> logger,
- IServerApplicationHost appHost,
IPluginManager pluginManager)
{
_logger = logger;
- _appHost = appHost;
_pluginManager = pluginManager;
}
@@ -51,7 +46,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("web/ConfigurationPages")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
+ public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
[FromQuery] bool? enableInMainMenu)
{
var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
@@ -77,38 +72,22 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
{
- IPlugin? plugin = null;
- Stream? stream = null;
-
- var isJs = false;
- var isTemplate = false;
-
var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
- if (altPage != null)
+ if (altPage == null)
{
- plugin = altPage.Item2;
- stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
-
- isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
- isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
+ return NotFound();
}
- if (plugin != null && stream != null)
+ IPlugin plugin = altPage.Item2;
+ string resourcePath = altPage.Item1.EmbeddedResourcePath;
+ Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
+ if (stream == null)
{
- if (isJs)
- {
- return File(stream, MimeTypes.GetMimeType("page.js"));
- }
-
- if (isTemplate)
- {
- return File(stream, MimeTypes.GetMimeType("page.html"));
- }
-
- return File(stream, MimeTypes.GetMimeType("page.html"));
+ _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
+ return NotFound();
}
- return NotFound();
+ return File(stream, MimeTypes.GetMimeType(resourcePath));
}
private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
@@ -116,11 +95,11 @@ namespace Jellyfin.Api.Controllers
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
}
- private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin)
+ private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
{
- if (plugin?.Instance is not IHasWebPages hasWebPages)
+ if (plugin.Instance is not IHasWebPages hasWebPages)
{
- return new List<Tuple<PluginPageInfo, IPlugin>>();
+ return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
}
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index e375645cf..62283d038 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -28,7 +28,6 @@ using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -52,8 +51,6 @@ namespace Jellyfin.Api.Controllers
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ILogger<DynamicHlsController> _logger;
@@ -72,12 +69,11 @@ namespace Jellyfin.Api.Controllers
/// <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="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> 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>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
public DynamicHlsController(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -87,15 +83,12 @@ namespace Jellyfin.Api.Controllers
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
ILogger<DynamicHlsController> logger,
- DynamicHlsHelper dynamicHlsHelper)
+ DynamicHlsHelper dynamicHlsHelper,
+ EncodingHelper encodingHelper)
{
- _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
-
_libraryManager = libraryManager;
_userManager = userManager;
_dlnaManager = dlnaManager;
@@ -104,12 +97,12 @@ namespace Jellyfin.Api.Controllers
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
+ _encodingHelper = encodingHelper;
+
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
}
@@ -203,7 +196,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -218,14 +211,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
var streamingRequest = new HlsVideoRequestDto
{
Id = itemId,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -249,28 +242,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -370,7 +363,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -385,14 +378,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
var streamingRequest = new HlsAudioRequestDto
{
Id = itemId,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -416,28 +409,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -533,7 +526,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -548,14 +541,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
- var cancellationTokenSource = new CancellationTokenSource();
+ using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
{
Id = itemId,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -579,28 +572,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -698,7 +691,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -713,14 +706,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
- var cancellationTokenSource = new CancellationTokenSource();
+ using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -744,28 +737,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -868,7 +861,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -883,14 +876,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var streamingRequest = new VideoRequestDto
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -914,28 +907,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -1040,7 +1033,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -1055,14 +1048,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -1086,28 +1079,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -1126,9 +1119,7 @@ namespace Jellyfin.Api.Controllers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
@@ -1146,7 +1137,7 @@ namespace Jellyfin.Api.Controllers
var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
var hlsVersion = isHlsInFmp4 ? "7" : "3";
- var builder = new StringBuilder();
+ var builder = new StringBuilder(128);
builder.AppendLine("#EXTM3U")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
@@ -1199,6 +1190,7 @@ namespace Jellyfin.Api.Controllers
throw new ArgumentException("StartTimeTicks is not allowed.");
}
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
@@ -1211,14 +1203,12 @@ namespace Jellyfin.Api.Controllers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
TranscodingJobType,
- cancellationTokenSource.Token)
+ cancellationToken)
.ConfigureAwait(false);
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
@@ -1237,7 +1227,7 @@ namespace Jellyfin.Api.Controllers
}
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
var released = false;
var startTranscoding = false;
@@ -1333,24 +1323,28 @@ namespace Jellyfin.Api.Controllers
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
- private double[] GetSegmentLengths(StreamState state)
- {
- var result = new List<double>();
-
- var ticks = state.RunTimeTicks ?? 0;
+ private static double[] GetSegmentLengths(StreamState state)
+ => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength);
- var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
+ internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength)
+ {
+ var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks;
+ var wholeSegments = runtimeTicks / segmentLengthTicks;
+ var remainingTicks = runtimeTicks % segmentLengthTicks;
- while (ticks > 0)
+ var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
+ var segments = new double[segmentsLen];
+ for (int i = 0; i < wholeSegments; i++)
{
- var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
-
- result.Add(TimeSpan.FromTicks(length).TotalSeconds);
+ segments[i] = segmentlength;
+ }
- ticks -= length;
+ if (remainingTicks != 0)
+ {
+ segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
}
- return result.ToArray();
+ return segments;
}
private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber)
@@ -1386,18 +1380,13 @@ namespace Jellyfin.Api.Controllers
}
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
{
- var outputFmp4HeaderArg = string.Empty;
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- if (isWindows)
+ var outputFmp4HeaderArg = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) switch
{
// on Windows, the path of fmp4 header file needs to be configured
- outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
- }
- else
- {
+ true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"",
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
- outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
- }
+ false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
+ };
segmentFormat = "fmp4" + outputFmp4HeaderArg;
}
@@ -1772,9 +1761,9 @@ namespace Jellyfin.Api.Controllers
private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
{
- var folder = Path.GetDirectoryName(playlist);
+ var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist));
- var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
+ var filePrefix = Path.GetFileNameWithoutExtension(playlist);
try
{
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 7bcf4674c..5aa457153 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
/// <response code="200">Genres returned.</response>
@@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers
NameStartsWithOrGreater = nameStartsWithOrGreater,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 25abe73ed..473bdc523 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -61,7 +61,13 @@ namespace Jellyfin.Api.Controllers
{
// TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path);
- file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+ var transcodePath = _serverConfigurationManager.GetTranscodePath();
+ file = Path.GetFullPath(Path.Combine(transcodePath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath))
+ {
+ return BadRequest("Invalid segment.");
+ }
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
}
@@ -81,7 +87,13 @@ namespace Jellyfin.Api.Controllers
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = playlistId + Path.GetExtension(Request.Path);
- file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+ var transcodePath = _serverConfigurationManager.GetTranscodePath();
+ file = Path.GetFullPath(Path.Combine(transcodePath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8")
+ {
+ return BadRequest("Invalid segment.");
+ }
return GetFileResult(file, file);
}
@@ -96,7 +108,9 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Videos/ActiveEncodings")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
+ public ActionResult StopEncodingProcess(
+ [FromQuery, Required] string deviceId,
+ [FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
return NoContent();
@@ -128,7 +142,12 @@ namespace Jellyfin.Api.Controllers
var file = segmentId + Path.GetExtension(Request.Path);
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
- file = Path.Combine(transcodeFolderPath, file);
+ file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath))
+ {
+ return BadRequest("Invalid segment.");
+ }
var normalizedPlaylistId = playlistId;
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 198dbc51f..e1b808098 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
: type;
var path = BaseItem.SupportedImageExtensions
- .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
+ .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i)))
.FirstOrDefault(System.IO.File.Exists);
if (path == null)
@@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
+ if (!path.StartsWith(_applicationPaths.GeneralPath))
+ {
+ return BadRequest("Invalid image path.");
+ }
+
var contentType = MimeTypes.GetMimeType(path);
return File(System.IO.File.OpenRead(path), contentType);
}
@@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
private ActionResult GetImageFile(string basePath, string theme, string? name)
{
- var themeFolder = Path.Combine(basePath, theme);
+ var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme));
+
if (Directory.Exists(themeFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
@@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
+ if (!path.StartsWith(basePath))
+ {
+ return BadRequest("Invalid image path.");
+ }
+
var contentType = MimeTypes.GetMimeType(path);
+
return PhysicalFile(path, contentType);
}
}
- var allFolder = Path.Combine(basePath, "all");
+ var allFolder = Path.GetFullPath(Path.Combine(basePath, "all"));
if (Directory.Exists(allFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
@@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
+ if (!path.StartsWith(basePath))
+ {
+ return BadRequest("Invalid image path.");
+ }
+
var contentType = MimeTypes.GetMimeType(path);
return PhysicalFile(path, contentType);
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index dc3634970..8f7500ac6 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@@ -196,6 +196,11 @@ namespace Jellyfin.Api.Controllers
}
var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage == null)
+ {
+ return NoContent();
+ }
+
try
{
System.IO.File.Delete(user.ProfileImage.Path);
@@ -235,6 +240,11 @@ namespace Jellyfin.Api.Controllers
}
var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage == null)
+ {
+ return NoContent();
+ }
+
try
{
System.IO.File.Delete(user.ProfileImage.Path);
@@ -392,7 +402,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
- [FromQuery] int newIndex)
+ [FromQuery, Required] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -480,6 +490,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
@@ -509,8 +521,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
[FromQuery] string? tag,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] double? percentPlayed,
@@ -539,7 +553,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -560,6 +575,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
@@ -589,8 +606,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
[FromQuery] string? tag,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] double? percentPlayed,
@@ -618,7 +637,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -638,6 +658,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
@@ -667,8 +689,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
[FromRoute, Required] string tag,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromRoute, Required] ImageFormat format,
[FromQuery] bool? addPlayedIndicator,
[FromRoute, Required] double percentPlayed,
@@ -697,7 +721,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -721,6 +746,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -741,7 +768,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetArtistImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -750,7 +777,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -776,7 +805,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -800,6 +830,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -820,7 +852,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -829,7 +861,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -855,7 +889,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -880,6 +915,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -900,7 +937,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -909,7 +946,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -934,7 +973,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -958,6 +998,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -978,7 +1020,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetMusicGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -987,7 +1029,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1013,7 +1057,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1038,6 +1083,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1058,7 +1105,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1067,7 +1114,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1092,7 +1141,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1116,6 +1166,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1136,7 +1188,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetPersonImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1145,7 +1197,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1171,7 +1225,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1196,6 +1251,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1216,7 +1273,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
- [FromQuery] string tag,
+ [FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1225,7 +1282,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1250,7 +1309,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1274,6 +1334,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1303,7 +1365,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1329,7 +1393,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1354,6 +1419,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1383,7 +1450,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1408,7 +1477,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1432,6 +1502,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1461,7 +1533,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1469,7 +1543,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageIndex)
{
var user = _userManager.GetUserById(userId);
- if (user == null)
+ if (user?.ProfileImage == null)
{
return NotFound();
}
@@ -1504,7 +1578,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1530,6 +1605,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1559,7 +1636,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
- [FromQuery] bool? cropWhitespace,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
@@ -1601,7 +1680,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
- cropWhitespace,
+ fillWidth,
+ fillHeight,
addPlayedIndicator,
blur,
backgroundColor,
@@ -1685,7 +1765,8 @@ namespace Jellyfin.Api.Controllers
int? width,
int? height,
int? quality,
- bool? cropWhitespace,
+ int? fillWidth,
+ int? fillHeight,
bool? addPlayedIndicator,
int? blur,
string? backgroundColor,
@@ -1727,8 +1808,6 @@ namespace Jellyfin.Api.Controllers
}
}
- cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art;
-
var outputFormats = GetOutputFormats(format);
TimeSpan? cacheDuration = null;
@@ -1748,11 +1827,13 @@ namespace Jellyfin.Api.Controllers
item,
itemId,
imageIndex,
+ width,
height,
- maxHeight,
maxWidth,
+ maxHeight,
+ fillWidth,
+ fillHeight,
quality,
- width,
addPlayedIndicator,
percentPlayed,
unplayedCount,
@@ -1760,7 +1841,6 @@ namespace Jellyfin.Api.Controllers
backgroundColor,
foregroundLayer,
imageInfo,
- cropWhitespace.Value,
outputFormats,
cacheDuration,
responseHeaders,
@@ -1779,17 +1859,15 @@ namespace Jellyfin.Api.Controllers
private ImageFormat[] GetClientSupportedFormats()
{
- var acceptTypes = Request.Headers[HeaderNames.Accept];
- var supportedFormats = new List<string>();
- if (acceptTypes.Count > 0)
+ var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
+ for (var i = 0; i < supportedFormats.Length; i++)
{
- foreach (var type in acceptTypes)
+ // Remove charsets etc. (anything after semi-colon)
+ var type = supportedFormats[i];
+ int index = type.IndexOf(';', StringComparison.Ordinal);
+ if (index != -1)
{
- int index = type.IndexOf(';', StringComparison.Ordinal);
- if (index != -1)
- {
- supportedFormats.Add(type.Substring(0, index));
- }
+ supportedFormats[i] = type.Substring(0, index);
}
}
@@ -1847,11 +1925,13 @@ namespace Jellyfin.Api.Controllers
BaseItem? item,
Guid itemId,
int? index,
+ int? width,
int? height,
- int? maxHeight,
int? maxWidth,
+ int? maxHeight,
+ int? fillWidth,
+ int? fillHeight,
int? quality,
- int? width,
bool? addPlayedIndicator,
double? percentPlayed,
int? unplayedCount,
@@ -1859,7 +1939,6 @@ namespace Jellyfin.Api.Controllers
string? backgroundColor,
string? foregroundLayer,
ItemImageInfo imageInfo,
- bool cropWhitespace,
IReadOnlyCollection<ImageFormat> supportedFormats,
TimeSpan? cacheDuration,
IDictionary<string, string> headers,
@@ -1872,7 +1951,6 @@ namespace Jellyfin.Api.Controllers
var options = new ImageProcessingOptions
{
- CropWhiteSpace = cropWhitespace,
Height = height,
ImageIndex = index ?? 0,
Image = imageInfo,
@@ -1880,6 +1958,8 @@ namespace Jellyfin.Api.Controllers
ItemId = itemId,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
+ FillHeight = fillHeight,
+ FillWidth = fillWidth,
Quality = quality ?? 100,
Width = width,
AddPlayedIndicator = addPlayedIndicator ?? false,
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index f061755c3..f232dffaa 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given album.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given playlist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given genre.
/// </summary>
/// <param name="name">The genre name.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -172,7 +172,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
[FromRoute, Required] string name,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given artist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given genre.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -243,7 +243,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -265,7 +265,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Creates an instant playlist based on a given song.
+ /// Creates an instant playlist based on a given item.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -300,6 +300,80 @@ namespace Jellyfin.Api.Controllers
return GetResult(items, user, limit, dtoOptions);
}
+ /// <summary>
+ /// Creates an instant playlist based on a given artist.
+ /// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <response code="200">Instant playlist returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+ [HttpGet("Artists/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetInstantMixFromArtists")]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
+ [FromQuery, Required] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ {
+ return GetInstantMixFromArtists(
+ id,
+ userId,
+ limit,
+ fields,
+ enableImages,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes);
+ }
+
+ /// <summary>
+ /// Creates an instant playlist based on a given genre.
+ /// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <response code="200">Instant playlist returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+ [HttpGet("MusicGenres/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetInstantMixFromMusicGenres instead")]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2(
+ [FromQuery, Required] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ {
+ return GetInstantMixFromMusicGenreById(
+ id,
+ userId,
+ limit,
+ fields,
+ enableImages,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes);
+ }
+
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
{
var list = items;
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index dfc68ffce..9fa307858 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -238,48 +238,6 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Gets a remote image.
- /// </summary>
- /// <param name="imageUrl">The image url.</param>
- /// <param name="providerName">The provider name.</param>
- /// <response code="200">Remote image retrieved.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
- /// </returns>
- [HttpGet("Items/RemoteSearch/Image")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesImageFile]
- public async Task<ActionResult> GetRemoteSearchImage(
- [FromQuery, Required] string imageUrl,
- [FromQuery, Required] string providerName)
- {
- var urlHash = imageUrl.GetMD5();
- var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
- try
- {
- var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
- if (System.IO.File.Exists(contentPath))
- {
- return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
- }
- }
- catch (FileNotFoundException)
- {
- // Means the file isn't cached yet
- }
- catch (IOException)
- {
- // Means the file isn't cached yet
- }
-
- await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
- var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
- return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
- }
-
- /// <summary>
/// Applies search criteria to an item and refreshes metadata.
/// </summary>
/// <param name="itemId">Item id.</param>
@@ -320,53 +278,5 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
-
- /// <summary>
- /// Downloads the image.
- /// </summary>
- /// <param name="providerName">Name of the provider.</param>
- /// <param name="url">The URL.</param>
- /// <param name="urlHash">The URL hash.</param>
- /// <param name="pointerCachePath">The pointer cache path.</param>
- /// <returns>Task.</returns>
- private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
- {
- using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
- if (result.Content.Headers.ContentType?.MediaType == null)
- {
- throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
- }
-
- var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
- var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
- var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
- Directory.CreateDirectory(directory);
- using (var stream = result.Content)
- {
- await using var fileStream = new FileStream(
- fullCachePath,
- FileMode.Create,
- FileAccess.Write,
- FileShare.Read,
- IODefaults.FileStreamBufferSize,
- true);
-
- await stream.CopyToAsync(fileStream).ConfigureAwait(false);
- }
-
- var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
-
- Directory.CreateDirectory(pointerCacheDirectory);
- await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Gets the full cache path.
- /// </summary>
- /// <param name="filename">The filename.</param>
- /// <returns>System.String.</returns>
- private string GetFullCachePath(string filename)
- => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
}
}
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 9e1a39853..a9f4a5a58 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
+ public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 2c9760f6d..35c27dd0e 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -143,7 +143,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItems(
- [FromQuery] Guid? userId,
+ [FromQuery] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
@@ -224,8 +224,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
- var user = userId.HasValue && !userId.Equals(Guid.Empty)
- ? _userManager.GetUserById(userId.Value)
+ var user = !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId)
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
@@ -246,8 +246,13 @@ namespace Jellyfin.Api.Controllers
folder = _libraryManager.GetUserRootFolder();
}
- if (folder is IHasCollectionType hasCollectionType
- && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ string? collectionType = null;
+ if (folder is IHasCollectionType hasCollectionType)
+ {
+ collectionType = hasCollectionType.CollectionType;
+ }
+
+ if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
recursive = true;
includeItemTypes = new[] { BaseItemKind.Playlist };
@@ -270,10 +275,11 @@ namespace Jellyfin.Api.Controllers
}
}
- if (!(item is UserRootFolder)
+ if (item is not UserRootFolder
&& !isInEnabledFolder
&& !user.HasPermission(PermissionKind.EnableAllFolders)
- && !user.HasPermission(PermissionKind.EnableAllChannels))
+ && !user.HasPermission(PermissionKind.EnableAllChannels)
+ && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index db4aa9668..4ed15e1d5 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path));
+ return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
}
/// <summary>
@@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <response code="204">Library scan started.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpGet("Library/Refresh")]
+ [HttpPost("Library/Refresh")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RefreshLibrary()
@@ -590,17 +590,17 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Reports that new movies have been added by an external source.
/// </summary>
- /// <param name="updates">A list of updated media paths.</param>
+ /// <param name="dto">The update paths.</param>
/// <response code="204">Report success.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Media/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
+ public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
{
- foreach (var item in updates)
+ foreach (var item in dto.Updates)
{
- _libraryMonitor.ReportFileSystemChanged(item.Path);
+ _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null."));
}
return NoContent();
@@ -666,7 +666,7 @@ namespace Jellyfin.Api.Controllers
}
// TODO determine non-ASCII validity.
- return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
+ return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true);
}
/// <summary>
@@ -777,7 +777,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
[FromQuery] string? libraryContentType,
- [FromQuery] bool isNewLibrary)
+ [FromQuery] bool isNewLibrary = false)
{
var result = new LibraryOptionsResultDto();
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 94995650c..be9127dd3 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string? name,
- [FromQuery] string? collectionType,
+ [FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false)
@@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Updates a media path.
/// </summary>
- /// <param name="name">The name of the library.</param>
- /// <param name="pathInfo">The path info.</param>
+ /// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path updated.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[HttpPost("Paths/Update")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateMediaPath(
- [FromQuery] string? name,
- [FromBody] MediaPathInfo? pathInfo)
+ public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
{
- if (string.IsNullOrWhiteSpace(name))
+ if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
{
- throw new ArgumentNullException(nameof(name));
+ throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
}
- _libraryManager.UpdateMediaPath(name, pathInfo);
+ _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 4d788ad7d..010a3b19a 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -18,6 +18,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
@@ -89,7 +90,7 @@ namespace Jellyfin.Api.Controllers
// nameof(LiveTvProgram)
},
// IsMovie = true
- OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
Limit = 7,
ParentId = parentIdGuid,
Recursive = true,
@@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers
{
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
- OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
Limit = 10,
IsFavoriteOrLiked = true,
ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
@@ -120,7 +121,7 @@ namespace Jellyfin.Api.Controllers
DtoOptions = dtoOptions
});
- var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
+ var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
// Get recently played directors
var recentDirectors = GetDirectors(mostRecentMovies)
.ToList();
@@ -191,7 +192,8 @@ namespace Jellyfin.Api.Controllers
foreach (var name in names)
{
- var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var items = _libraryManager.GetItemList(
+ new InternalItemsQuery(user)
{
Person = name,
// Account for duplicates by imdb id, since the database doesn't support this yet
@@ -299,9 +301,8 @@ namespace Jellyfin.Api.Controllers
private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
{
- var people = _libraryManager.GetPeople(new InternalPeopleQuery
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
{
- ExcludePersonTypes = new[] { PersonType.Director },
MaxListOrder = 3
});
@@ -315,10 +316,9 @@ namespace Jellyfin.Api.Controllers
private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
{
- var people = _libraryManager.GetPeople(new InternalPeopleQuery
- {
- PersonTypes = new[] { PersonType.Director }
- });
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery(
+ new[] { PersonType.Director },
+ Array.Empty<string>()));
var itemIds = items.Select(i => i.Id).ToList();
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 7f7058b5e..27eec2b9a 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
/// <response code="200">Music genres returned.</response>
@@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers
NameStartsWithOrGreater = nameStartsWithOrGreater,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index 0ceda6815..420630cdf 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;
@@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Sends a notification to all admins.
/// </summary>
- /// <param name="url">The URL of the notification.</param>
- /// <param name="level">The level of the notification.</param>
- /// <param name="name">The name of the notification.</param>
- /// <param name="description">The description of the notification.</param>
+ /// <param name="notificationDto">The notification request.</param>
/// <response code="204">Notification sent.</response>
/// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("Admin")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateAdminNotification(
- [FromQuery] string? url,
- [FromQuery] NotificationLevel? level,
- [FromQuery] string name = "",
- [FromQuery] string description = "")
+ public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
{
var notification = new NotificationRequest
{
- Name = name,
- Description = description,
- Url = url,
- Level = level ?? NotificationLevel.Normal,
+ Name = notificationDto.Name,
+ Description = notificationDto.Description,
+ Url = notificationDto.Url,
+ Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
UserIds = _userManager.Users
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
.Select(user => user.Id)
@@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers
};
_notificationManager.SendNotification(notification, CancellationToken.None);
-
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 70a94e27c..b98307f87 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -94,10 +94,10 @@ namespace Jellyfin.Api.Controllers
}
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
- var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
+ var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery(
+ personTypes,
+ excludePersonTypes)
{
- PersonTypes = personTypes,
- ExcludePersonTypes = excludePersonTypes,
NameContains = searchTerm,
User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index ec7b84ff6..f256c8c25 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/Playing/Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
+ public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
return NoContent();
@@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] PlayMethod playMethod,
+ [FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
- [FromQuery] string playSessionId,
+ [FromQuery] string? playSessionId,
[FromQuery] bool canSeek = false)
{
var playbackStartInfo = new PlaybackStartInfo
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
- PlayMethod = playMethod,
+ PlayMethod = playMethod ?? PlayMethod.Transcode,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId
};
@@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? volumeLevel,
- [FromQuery] PlayMethod playMethod,
+ [FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
- [FromQuery] string playSessionId,
- [FromQuery] RepeatMode repeatMode,
+ [FromQuery] string? playSessionId,
+ [FromQuery] RepeatMode? repeatMode,
[FromQuery] bool isPaused = false,
[FromQuery] bool isMuted = false)
{
@@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
VolumeLevel = volumeLevel,
- PlayMethod = playMethod,
+ PlayMethod = playMethod ?? PlayMethod.Transcode,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId,
- RepeatMode = repeatMode
+ RepeatMode = repeatMode ?? RepeatMode.RepeatNone
};
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
@@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers
return _userDataRepository.GetUserDataDto(item, user);
}
- private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
+ private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
{
if (method == PlayMethod.Transcode)
{
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index a5aa9bfca..7a6130719 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -12,7 +12,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Authorization;
@@ -45,7 +44,7 @@ namespace Jellyfin.Api.Controllers
{
_installationManager = installationManager;
_pluginManager = pluginManager;
- _serializerOptions = JsonDefaults.GetOptions();
+ _serializerOptions = JsonDefaults.Options;
_config = config;
}
@@ -208,12 +207,7 @@ namespace Jellyfin.Api.Controllers
var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId));
// Select the un-instanced one first.
- var plugin = plugins.FirstOrDefault(p => p.Instance == null);
- if (plugin == null)
- {
- // Then by the status.
- plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
- }
+ var plugin = plugins.FirstOrDefault(p => p.Instance == null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
if (plugin != null)
{
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 5284888d8..ec836f43e 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -146,58 +146,6 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Gets a remote image.
- /// </summary>
- /// <param name="imageUrl">The image url.</param>
- /// <response code="200">Remote image returned.</response>
- /// <response code="404">Remote image not found.</response>
- /// <returns>Image Stream.</returns>
- [HttpGet("Images/Remote")]
- [Produces(MediaTypeNames.Application.Octet)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
- {
- var urlHash = imageUrl.ToString().GetMD5();
- var pointerCachePath = GetFullCachePath(urlHash.ToString());
-
- string? contentPath = null;
- var hasFile = false;
-
- try
- {
- contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
- if (System.IO.File.Exists(contentPath))
- {
- hasFile = true;
- }
- }
- catch (FileNotFoundException)
- {
- // The file isn't cached yet
- }
- catch (IOException)
- {
- // The file isn't cached yet
- }
-
- if (!hasFile)
- {
- await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
- contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
- }
-
- if (string.IsNullOrEmpty(contentPath))
- {
- return NotFound();
- }
-
- var contentType = MimeTypes.GetMimeType(contentPath);
- return PhysicalFile(contentPath, contentType);
- }
-
- /// <summary>
/// Downloads a remote image for an item.
/// </summary>
/// <param name="itemId">Item Id.</param>
@@ -259,7 +207,8 @@ namespace Jellyfin.Api.Controllers
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(fullCacheDirectory);
- await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 6c22050a7..73bdf9018 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -228,10 +228,7 @@ namespace Jellyfin.Api.Controllers
itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
}
- if (itemWithImage == null)
- {
- itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb);
- }
+ itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb);
if (itemWithImage != null)
{
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index e2269a2ce..7bd0b6918 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -153,6 +153,10 @@ namespace Jellyfin.Api.Controllers
/// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
/// <param name="startPositionTicks">The starting position of the first item.</param>
+ /// <param name="mediaSourceId">Optional. The media source id.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param>
+ /// <param name="startIndex">Optional. The start index.</param>
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Playing")]
@@ -162,13 +166,21 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
- [FromQuery] long? startPositionTicks)
+ [FromQuery] long? startPositionTicks,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] int? startIndex)
{
var playRequest = new PlayRequest
{
ItemIds = itemIds,
StartPositionTicks = startPositionTicks,
- PlayCommand = playCommand
+ PlayCommand = playCommand,
+ MediaSourceId = mediaSourceId,
+ AudioStreamIndex = audioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ StartIndex = startIndex
};
_sessionManager.SendPlayCommand(
@@ -301,9 +313,7 @@ namespace Jellyfin.Api.Controllers
/// Issues a command to a client to display a message to the user.
/// </summary>
/// <param name="sessionId">The session id.</param>
- /// <param name="text">The message test.</param>
- /// <param name="header">The message header.</param>
- /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
+ /// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param>
/// <response code="204">Message sent.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Message")]
@@ -311,16 +321,12 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendMessageCommand(
[FromRoute, Required] string sessionId,
- [FromQuery, Required] string text,
- [FromQuery] string? header,
- [FromQuery] long? timeoutMs)
+ [FromBody, Required] MessageCommand command)
{
- var command = new MessageCommand
+ if (string.IsNullOrWhiteSpace(command.Header))
{
- Header = string.IsNullOrEmpty(header) ? "Message from Server" : header,
- TimeoutMs = timeoutMs,
- Text = text
- };
+ command.Header = "Message from Server";
+ }
_sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index d9cb34557..a01a617fc 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -132,7 +132,10 @@ namespace Jellyfin.Api.Controllers
{
var user = _userManager.Users.First();
- user.Username = startupUserDto.Name;
+ if (startupUserDto.Name != null)
+ {
+ user.Username = startupUserDto.Name;
+ }
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 16a47f2d8..b473574e0 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -182,6 +182,10 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets subtitles in a specified format.
/// </summary>
+ /// <param name="routeItemId">The (route) item id.</param>
+ /// <param name="routeMediaSourceId">The (route) media source id.</param>
+ /// <param name="routeIndex">The (route) subtitle stream index.</param>
+ /// <param name="routeFormat">The (route) format of the returned subtitle.</param>
/// <param name="itemId">The item id.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="index">The subtitle stream index.</param>
@@ -189,22 +193,32 @@ namespace Jellyfin.Api.Controllers
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
- /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
+ /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
- [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
+ [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public async Task<ActionResult> GetSubtitle(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string mediaSourceId,
- [FromRoute, Required] int index,
- [FromRoute, Required] string format,
+ [FromRoute, Required] Guid routeItemId,
+ [FromRoute, Required] string routeMediaSourceId,
+ [FromRoute, Required] int routeIndex,
+ [FromRoute, Required] string routeFormat,
+ [FromQuery, ParameterObsolete] Guid? itemId,
+ [FromQuery, ParameterObsolete] string? mediaSourceId,
+ [FromQuery, ParameterObsolete] int? index,
+ [FromQuery, ParameterObsolete] string? format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false,
[FromQuery] long startPositionTicks = 0)
{
+ // Set parameters to route value if not provided via query.
+ itemId ??= routeItemId;
+ mediaSourceId ??= routeMediaSourceId;
+ index ??= routeIndex;
+ format ??= routeFormat;
+
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
{
format = "json";
@@ -212,9 +226,9 @@ namespace Jellyfin.Api.Controllers
if (string.IsNullOrEmpty(format))
{
- var item = (Video)_libraryManager.GetItemById(itemId);
+ var item = (Video)_libraryManager.GetItemById(itemId.Value);
- var idString = itemId.ToString("N", CultureInfo.InvariantCulture);
+ var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
@@ -226,7 +240,7 @@ namespace Jellyfin.Api.Controllers
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
{
- await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
+ await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
using var reader = new StreamReader(stream);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
@@ -238,9 +252,9 @@ namespace Jellyfin.Api.Controllers
return File(
await EncodeSubtitles(
- itemId,
+ itemId.Value,
mediaSourceId,
- index,
+ index.Value,
format,
startPositionTicks,
endPositionTicks,
@@ -251,30 +265,44 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets subtitles in a specified format.
/// </summary>
+ /// <param name="routeItemId">The (route) item id.</param>
+ /// <param name="routeMediaSourceId">The (route) media source id.</param>
+ /// <param name="routeIndex">The (route) subtitle stream index.</param>
+ /// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param>
+ /// <param name="routeFormat">The (route) format of the returned subtitle.</param>
/// <param name="itemId">The item id.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="index">The subtitle stream index.</param>
- /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
+ /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
/// <param name="format">The format of the returned subtitle.</param>
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
- [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
+ [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public Task<ActionResult> GetSubtitleWithTicks(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string mediaSourceId,
- [FromRoute, Required] int index,
- [FromRoute, Required] long startPositionTicks,
- [FromRoute, Required] string format,
+ [FromRoute, Required] Guid routeItemId,
+ [FromRoute, Required] string routeMediaSourceId,
+ [FromRoute, Required] int routeIndex,
+ [FromRoute, Required] long routeStartPositionTicks,
+ [FromRoute, Required] string routeFormat,
+ [FromQuery, ParameterObsolete] Guid? itemId,
+ [FromQuery, ParameterObsolete] string? mediaSourceId,
+ [FromQuery, ParameterObsolete] int? index,
+ [FromQuery, ParameterObsolete] long? startPositionTicks,
+ [FromQuery, ParameterObsolete] string? format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false)
{
return GetSubtitle(
+ routeItemId,
+ routeMediaSourceId,
+ routeIndex,
+ routeFormat,
itemId,
mediaSourceId,
index,
@@ -282,7 +310,7 @@ namespace Jellyfin.Api.Controllers
endPositionTicks,
copyTimestamps,
addVttTimeMap,
- startPositionTicks);
+ startPositionTicks ?? routeStartPositionTicks);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index a55f13e66..a811a29c3 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions().AddClientFields(Request);
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{
- OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
MediaTypes = mediaType,
IncludeItemTypes = type,
IsVirtualItem = false,
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index dd3836551..5cb7468b2 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
- [FromQuery] Guid? userId,
+ [FromQuery] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index e1c67f830..ffb726fab 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -65,6 +65,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
@@ -81,6 +82,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
+ [FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false)
{
@@ -97,7 +99,8 @@ namespace Jellyfin.Api.Controllers
StartIndex = startIndex,
UserId = userId ?? Guid.Empty,
EnableTotalRecordCount = enableTotalRecordCount,
- DisableFirstEpisode = disableFirstEpisode
+ DisableFirstEpisode = disableFirstEpisode,
+ NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue
},
options);
@@ -155,7 +158,7 @@ namespace Jellyfin.Api.Controllers
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { nameof(Episode) },
- OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
MinPremiereDate = minPremiereDate,
StartIndex = startIndex,
Limit = limit,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index bacd95bac..679f055bc 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] bool? enableRemoteMedia,
- [FromQuery] bool breakOnNonKeyFrames,
+ [FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
@@ -219,11 +219,11 @@ namespace Jellyfin.Api.Controllers
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Hls,
- RequireAvc = true,
- DeInterlace = true,
- RequireNonAnamorphic = true,
- EnableMpegtsM2TsMode = true,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ RequireAvc = false,
+ DeInterlace = false,
+ RequireNonAnamorphic = false,
+ EnableMpegtsM2TsMode = false,
+ TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
Context = EncodingContext.Static,
StreamOptions = new Dictionary<string, string>(),
EnableAdaptiveBitrateStreaming = true
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
CopyTimestamps = true,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Embed,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
Context = EncodingContext.Static
};
@@ -298,9 +298,9 @@ namespace Jellyfin.Api.Controllers
{
Type = DlnaProfileType.Audio,
Context = EncodingContext.Streaming,
- Container = transcodingContainer,
- AudioCodec = audioCodec,
- Protocol = transcodingProtocol,
+ Container = transcodingContainer ?? "mp3",
+ AudioCodec = audioCodec ?? "mp3",
+ Protocol = transcodingProtocol ?? "http",
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
}
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 43ee309b7..b13db4baa 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -21,6 +21,7 @@ using MediaBrowser.Model.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
@@ -36,6 +37,7 @@ namespace Jellyfin.Api.Controllers
private readonly IDeviceManager _deviceManager;
private readonly IAuthorizationContext _authContext;
private readonly IServerConfigurationManager _config;
+ private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="UserController"/> class.
@@ -46,13 +48,15 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
public UserController(
IUserManager userManager,
ISessionManager sessionManager,
INetworkManager networkManager,
IDeviceManager deviceManager,
IAuthorizationContext authContext,
- IServerConfigurationManager config)
+ IServerConfigurationManager config,
+ ILogger<UserController> logger)
{
_userManager = userManager;
_sessionManager = sessionManager;
@@ -60,6 +64,7 @@ namespace Jellyfin.Api.Controllers
_deviceManager = deviceManager;
_authContext = authContext;
_config = config;
+ _logger = logger;
}
/// <summary>
@@ -118,7 +123,7 @@ namespace Jellyfin.Api.Controllers
return NotFound("User not found");
}
- var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp());
+ var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString());
return result;
}
@@ -172,11 +177,9 @@ namespace Jellyfin.Api.Controllers
return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed.");
}
- // Password should always be null
AuthenticateUserByName request = new AuthenticateUserByName
{
Username = user.Username,
- Password = null,
Pw = pw
};
return await AuthenticateUserByName(request).ConfigureAwait(false);
@@ -203,8 +206,7 @@ namespace Jellyfin.Api.Controllers
DeviceId = auth.DeviceId,
DeviceName = auth.Device,
Password = request.Pw,
- PasswordSha1 = request.Password,
- RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(),
+ RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(),
Username = request.Username
}).ConfigureAwait(false);
@@ -291,7 +293,7 @@ namespace Jellyfin.Api.Controllers
user.Username,
request.CurrentPw,
request.CurrentPw,
- HttpContext.GetNormalizedRemoteIp(),
+ HttpContext.GetNormalizedRemoteIp().ToString(),
false).ConfigureAwait(false);
if (success == null)
@@ -483,7 +485,7 @@ namespace Jellyfin.Api.Controllers
await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
}
- var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp());
+ var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString());
return result;
}
@@ -498,8 +500,14 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
{
+ var ip = HttpContext.GetNormalizedRemoteIp();
var isLocal = HttpContext.IsLocal()
- || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp());
+ || _networkManager.IsInLocalNetwork(ip);
+
+ if (isLocal)
+ {
+ _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip);
+ }
var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
@@ -581,7 +589,7 @@ namespace Jellyfin.Api.Controllers
var result = users
.OrderBy(u => u.Username)
- .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp()));
+ .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString()));
return result;
}
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index ba51aa43e..6a720b1a4 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -48,9 +48,6 @@ namespace Jellyfin.Api.Controllers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ILogger<VideoHlsController> _logger;
@@ -60,9 +57,6 @@ namespace Jellyfin.Api.Controllers
/// Initializes a new instance of the <see cref="VideoHlsController"/> class.
/// </summary>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
/// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
@@ -72,11 +66,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
/// <param name="logger">Instance of the <see cref="ILogger{VideoHlsController}"/>.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
public VideoHlsController(
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDlnaManager dlnaManager,
IUserManager userManger,
IAuthorizationContext authorizationContext,
@@ -85,10 +77,9 @@ namespace Jellyfin.Api.Controllers
IServerConfigurationManager serverConfigurationManager,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
- ILogger<VideoHlsController> logger)
+ ILogger<VideoHlsController> logger,
+ EncodingHelper encodingHelper)
{
- _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
-
_dlnaManager = dlnaManager;
_authContext = authorizationContext;
_userManager = userManger;
@@ -96,12 +87,11 @@ namespace Jellyfin.Api.Controllers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_logger = logger;
+ _encodingHelper = encodingHelper;
+
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
}
@@ -198,7 +188,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -213,7 +203,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -223,7 +213,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -247,34 +237,35 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
};
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
@@ -285,9 +276,7 @@ namespace Jellyfin.Api.Controllers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 44dc63952..dc64a0f1b 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -49,12 +49,10 @@ namespace Jellyfin.Api.Controllers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly EncodingHelper _encodingHelper;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
@@ -69,12 +67,10 @@ namespace Jellyfin.Api.Controllers
/// <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="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> 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>
public VideosController(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -84,12 +80,10 @@ namespace Jellyfin.Api.Controllers
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
- IHttpClientFactory httpClientFactory)
+ IHttpClientFactory httpClientFactory,
+ EncodingHelper encodingHelper)
{
_libraryManager = libraryManager;
_userManager = userManager;
@@ -99,12 +93,10 @@ namespace Jellyfin.Api.Controllers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
+ _encodingHelper = encodingHelper;
}
/// <summary>
@@ -217,9 +209,7 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Please supply at least two videos to merge.");
}
- var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
-
- var primaryVersion = videosWithVersions.FirstOrDefault();
+ var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
if (primaryVersion == null)
{
primaryVersion = items
@@ -364,7 +354,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -379,16 +369,17 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
{
Id = itemId,
Container = container,
- Static = @static ?? true,
+ Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -412,28 +403,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? true,
+ CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? true,
- DeInterlace = deInterlace ?? true,
- RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -446,9 +437,7 @@ namespace Jellyfin.Api.Controllers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
@@ -517,8 +506,7 @@ namespace Jellyfin.Api.Controllers
// Need to start ffmpeg (because media can't be returned directly)
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
- var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
@@ -540,7 +528,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -569,7 +557,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -583,8 +571,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/{stream=stream}.{container}")]
- [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
+ [HttpGet("{itemId}/stream.{container}")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesVideoFile]
public Task<ActionResult> GetVideoStreamByContainer(
@@ -620,7 +608,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -635,7 +623,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext context,
+ [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
return GetVideoStream(
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index f2abd515d..06173315a 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
@@ -113,14 +112,5 @@ namespace Jellyfin.Api.Extensions
return dtoOptions;
}
-
- /// <summary>
- /// Check if DtoOptions contains field.
- /// </summary>
- /// <param name="dtoOptions">DtoOptions object.</param>
- /// <param name="field">Field to check.</param>
- /// <returns>Field existence.</returns>
- internal static bool ContainsField(this DtoOptions dtoOptions, ItemFields field)
- => dtoOptions.Fields != null && dtoOptions.Fields.Contains(field);
}
}
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index 21ec2d32f..264131905 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Net.Http;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
@@ -33,13 +32,11 @@ namespace Jellyfin.Api.Helpers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly EncodingHelper _encodingHelper;
/// <summary>
/// Initializes a new instance of the <see cref="AudioHelper"/> class.
@@ -51,13 +48,11 @@ namespace Jellyfin.Api.Helpers
/// <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="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> 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,
IAuthorizationContext authContext,
@@ -66,13 +61,11 @@ namespace Jellyfin.Api.Helpers
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
IHttpClientFactory httpClientFactory,
- IHttpContextAccessor httpContextAccessor)
+ IHttpContextAccessor httpContextAccessor,
+ EncodingHelper encodingHelper)
{
_dlnaManager = dlnaManager;
_authContext = authContext;
@@ -81,13 +74,11 @@ namespace Jellyfin.Api.Helpers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor;
+ _encodingHelper = encodingHelper;
}
/// <summary>
@@ -106,6 +97,8 @@ namespace Jellyfin.Api.Helpers
}
bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
+
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
using var state = await StreamingHelpers.GetStreamingState(
@@ -117,9 +110,7 @@ namespace Jellyfin.Api.Helpers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
@@ -188,8 +179,7 @@ namespace Jellyfin.Api.Helpers
// Need to start ffmpeg (because media can't be returned directly)
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
- var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 92ff42b49..dc5d6715b 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -40,14 +40,12 @@ namespace Jellyfin.Api.Helpers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
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;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
@@ -59,14 +57,12 @@ namespace Jellyfin.Api.Helpers
/// <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="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> 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>
public DynamicHlsHelper(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -75,14 +71,12 @@ namespace Jellyfin.Api.Helpers
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
INetworkManager networkManager,
ILogger<DynamicHlsHelper> logger,
- IHttpContextAccessor httpContextAccessor)
+ IHttpContextAccessor httpContextAccessor,
+ EncodingHelper encodingHelper)
{
_libraryManager = libraryManager;
_userManager = userManager;
@@ -91,14 +85,12 @@ namespace Jellyfin.Api.Helpers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_networkManager = networkManager;
_logger = logger;
_httpContextAccessor = httpContextAccessor;
+ _encodingHelper = encodingHelper;
}
/// <summary>
@@ -114,6 +106,7 @@ namespace Jellyfin.Api.Helpers
bool enableAdaptiveBitrateStreaming)
{
var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
return await GetMasterPlaylistInternal(
streamingRequest,
@@ -144,9 +137,7 @@ namespace Jellyfin.Api.Helpers
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
+ _encodingHelper,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
@@ -171,13 +162,15 @@ namespace Jellyfin.Api.Helpers
var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
// from universal audio service
- if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
+ if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
+ && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
{
queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
}
// from universal audio service
- if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
+ if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
+ && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
{
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
}
@@ -222,12 +215,11 @@ namespace Jellyfin.Api.Helpers
{
// Force HEVC Main Profile and disable video stream copy.
state.OutputVideoCodec = "hevc";
- var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(",", requestedVideoProfiles), "main");
+ var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
sdrVideoUrl += "&AllowVideoStreamCopy=false";
- EncodingHelper encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
- var sdrOutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
- var sdrOutputAudioBitrate = encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
+ var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
+ var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
@@ -431,7 +423,7 @@ namespace Jellyfin.Api.Helpers
}
}
- private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, string ipAddress)
+ private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
{
// Within the local network this will likely do more harm than good.
if (_networkManager.IsInLocalNetwork(ipAddress))
@@ -560,13 +552,13 @@ namespace Jellyfin.Api.Helpers
profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
- profileString = profileString ?? "high";
+ profileString ??= "high";
}
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
- profileString = profileString ?? "main";
+ profileString ??= "main";
}
}
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index cfa2c1229..b0fd59e5e 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -39,14 +39,15 @@ namespace Jellyfin.Api.Helpers
}
// Can't dispose the response as it's required up the call chain.
- var response = await httpClient.GetAsync(new Uri(state.MediaPath)).ConfigureAwait(false);
+ var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
var contentType = response.Content.Headers.ContentType?.ToString();
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
if (isHeadRequest)
{
- return new FileContentResult(Array.Empty<byte>(), contentType);
+ httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
+ return new OkResult();
}
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
@@ -68,10 +69,10 @@ namespace Jellyfin.Api.Helpers
{
httpContext.Response.ContentType = contentType;
- // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
+ // if the request is a head request, return an OkResult (200) with the same headers as it would with a GET request
if (isHeadRequest)
{
- return new NoContentResult();
+ return new OkResult();
}
return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
@@ -107,7 +108,8 @@ namespace Jellyfin.Api.Helpers
// Headers only
if (isHeadRequest)
{
- return new FileContentResult(Array.Empty<byte>(), contentType);
+ httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
+ return new OkResult();
}
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index 18e23fb5c..d0666034e 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -118,10 +118,7 @@ namespace Jellyfin.Api.Helpers
/// <returns>The playlist text as a string.</returns>
public static string GetLivePlaylistText(string path, StreamState state)
{
- using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- using var reader = new StreamReader(stream);
-
- var text = reader.ReadToEnd();
+ var text = File.ReadAllText(path);
var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 0d8315dee..295cfaf08 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Linq;
+using System.Net;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -179,7 +180,7 @@ namespace Jellyfin.Api.Helpers
bool enableTranscoding,
bool allowVideoStreamCopy,
bool allowAudioStreamCopy,
- string ipAddress)
+ IPAddress ipAddress)
{
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
@@ -282,6 +283,7 @@ namespace Jellyfin.Api.Helpers
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}
@@ -307,7 +309,7 @@ namespace Jellyfin.Api.Helpers
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
+ && user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectStream = true;
}
@@ -326,6 +328,7 @@ namespace Jellyfin.Api.Helpers
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}
@@ -353,6 +356,7 @@ namespace Jellyfin.Api.Helpers
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
else
@@ -390,6 +394,7 @@ namespace Jellyfin.Api.Helpers
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+ mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}
@@ -523,7 +528,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="type">Dlna profile type.</param>
public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
{
- mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
+ mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
}
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
@@ -551,7 +556,7 @@ namespace Jellyfin.Api.Helpers
}
}
- private int? GetMaxBitrate(int? clientMaxBitrate, User user, string ipAddress)
+ private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
{
var maxBitrate = clientMaxBitrate;
var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
index 8bddf00d5..963e17724 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
@@ -71,7 +71,8 @@ namespace Jellyfin.Api.Helpers
/// <returns>A <see cref="Task"/>.</returns>
public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
{
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
+ using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken);
+ cancellationToken = linkedCancellationTokenSource.Token;
try
{
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 94856e03e..56585aeab 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Helpers
authorization.Version,
authorization.DeviceId,
authorization.Device,
- request.HttpContext.GetNormalizedRemoteIp(),
+ request.HttpContext.GetNormalizedRemoteIp().ToString(),
user);
if (session == null)
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 4957ee8b8..8cffe9c4c 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -41,9 +41,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> 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="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> 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>
@@ -59,16 +57,13 @@ namespace Jellyfin.Api.Helpers
ILibraryManager libraryManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
+ EncodingHelper encodingHelper,
IDlnaManager dlnaManager,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
TranscodingJobType transcodingJobType,
CancellationToken cancellationToken)
{
- EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
// Parse the DLNA time seek header
if (!streamingRequest.StartTimeTicks.HasValue)
{
@@ -121,14 +116,14 @@ namespace Jellyfin.Api.Helpers
if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
{
state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
+ state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec)
?? state.SupportedAudioCodecs.FirstOrDefault();
}
if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
{
state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
- state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
+ state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleCodec)
?? state.SupportedSubtitleCodecs.FirstOrDefault();
}
@@ -183,7 +178,7 @@ namespace Jellyfin.Api.Helpers
if (string.IsNullOrEmpty(containerInternal))
{
containerInternal = streamingRequest.Static ?
- StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio)
+ StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
: GetOutputFileExtension(state);
}
@@ -245,7 +240,7 @@ namespace Jellyfin.Api.Helpers
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
? GetOutputFileExtension(state)
- : ('.' + state.OutputContainer);
+ : ("." + state.OutputContainer);
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
@@ -297,16 +292,14 @@ namespace Jellyfin.Api.Helpers
}
}
- if (profile == null)
- {
- profile = dlnaManager.GetDefaultProfile();
- }
+ profile ??= dlnaManager.GetDefaultProfile();
var audioCodec = state.ActualOutputAudioCodec;
if (!state.IsVideoRequest)
{
- responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader(
+ responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader(
+ profile,
state.OutputContainer,
audioCodec,
state.OutputAudioBitrate,
@@ -323,7 +316,7 @@ namespace Jellyfin.Api.Helpers
responseHeaders.Add(
"contentFeatures.dlna.org",
- new ContentFeatureBuilder(profile).BuildVideoHeader(state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, 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);
+ ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, 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);
}
}
@@ -508,17 +501,15 @@ namespace Jellyfin.Api.Helpers
private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
{
- var headers = request.Headers;
-
if (!string.IsNullOrWhiteSpace(deviceProfileId))
{
state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
- }
- else if (!string.IsNullOrWhiteSpace(deviceProfileId))
- {
- var caps = deviceManager.GetCapabilities(deviceProfileId);
- state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
+ if (state.DeviceProfile == null)
+ {
+ var caps = deviceManager.GetCapabilities(deviceProfileId);
+ state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
+ }
}
var profile = state.DeviceProfile;
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 240d132b1..c295af7eb 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -22,7 +22,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Helpers
@@ -62,8 +61,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
public TranscodingJobHelper(
ILogger<TranscodingJobHelper> logger,
@@ -73,8 +71,7 @@ namespace Jellyfin.Api.Helpers
IServerConfigurationManager serverConfigurationManager,
ISessionManager sessionManager,
IAuthorizationContext authorizationContext,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
+ EncodingHelper encodingHelper,
ILoggerFactory loggerFactory)
{
_logger = logger;
@@ -84,8 +81,8 @@ namespace Jellyfin.Api.Helpers
_serverConfigurationManager = serverConfigurationManager;
_sessionManager = sessionManager;
_authorizationContext = authorizationContext;
+ _encodingHelper = encodingHelper;
_loggerFactory = loggerFactory;
- _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
DeleteEncodedMediaCache();
@@ -272,7 +269,7 @@ namespace Jellyfin.Api.Helpers
{
_activeTranscodingJobs.Remove(job);
- if (!job.CancellationTokenSource!.IsCancellationRequested)
+ if (job.CancellationTokenSource?.IsCancellationRequested == false)
{
job.CancellationTokenSource.Cancel();
}
@@ -382,7 +379,9 @@ namespace Jellyfin.Api.Helpers
/// <param name="outputFilePath">The output file path.</param>
private void DeleteHlsPartialStreamFiles(string outputFilePath)
{
- var directory = Path.GetDirectoryName(outputFilePath);
+ var directory = Path.GetDirectoryName(outputFilePath)
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+
var name = Path.GetFileNameWithoutExtension(outputFilePath);
var filesToDelete = _fileSystem.GetFilePaths(directory)
@@ -752,7 +751,7 @@ namespace Jellyfin.Api.Helpers
_logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
}
- process.Dispose();
+ job.Dispose();
}
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index ef105fdce..bd7da9b06 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -15,10 +15,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.3" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
- <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.0.2" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.4" />
+ <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.1.4" />
</ItemGroup>
<ItemGroup>
@@ -28,7 +28,6 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
diff --git a/Jellyfin.Api/Models/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
index a7bbe42fe..ec4a0d1a1 100644
--- a/Jellyfin.Api/Models/ConfigurationPageInfo.cs
+++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs
@@ -1,6 +1,5 @@
using System;
using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
namespace Jellyfin.Api.Models
@@ -26,6 +25,14 @@ namespace Jellyfin.Api.Models
}
/// <summary>
+ /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class.
+ /// </summary>
+ public ConfigurationPageInfo()
+ {
+ Name = string.Empty;
+ }
+
+ /// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
index 991dbfc50..f93638898 100644
--- a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
@@ -1,4 +1,7 @@
-namespace Jellyfin.Api.Models.LibraryDtos
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Api.Models.LibraryDtos
{
/// <summary>
/// Media Update Info Dto.
@@ -6,14 +9,8 @@
public class MediaUpdateInfoDto
{
/// <summary>
- /// Gets or sets media path.
- /// </summary>
- public string? Path { get; set; }
-
- /// <summary>
- /// Gets or sets media update type.
- /// Created, Modified, Deleted.
+ /// Gets or sets the list of updates.
/// </summary>
- public string? UpdateType { get; set; }
+ public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>();
}
}
diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
new file mode 100644
index 000000000..852315b92
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
@@ -0,0 +1,19 @@
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+ /// <summary>
+ /// The media update info path.
+ /// </summary>
+ public class MediaUpdateInfoPathDto
+ {
+ /// <summary>
+ /// Gets or sets media path.
+ /// </summary>
+ public string? Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets media update type.
+ /// Created, Modified, Deleted.
+ /// </summary>
+ public string? UpdateType { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs
new file mode 100644
index 000000000..fbd4985f9
--- /dev/null
+++ b/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel.DataAnnotations;
+using MediaBrowser.Model.Configuration;
+
+namespace Jellyfin.Api.Models.LibraryStructureDto
+{
+ /// <summary>
+ /// Update library options dto.
+ /// </summary>
+ public class UpdateMediaPathRequestDto
+ {
+ /// <summary>
+ /// Gets or sets the library name.
+ /// </summary>
+ [Required]
+ public string Name { get; set; } = null!;
+
+ /// <summary>
+ /// Gets or sets library folder path information.
+ /// </summary>
+ [Required]
+ public MediaPathInfo PathInfo { get; set; } = null!;
+ }
+}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 588ce717c..8913180e4 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Json.Converters;
diff --git a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs
new file mode 100644
index 000000000..2c3a6282f
--- /dev/null
+++ b/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs
@@ -0,0 +1,30 @@
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+ /// <summary>
+ /// The admin notification dto.
+ /// </summary>
+ public class AdminNotificationDto
+ {
+ /// <summary>
+ /// Gets or sets the notification name.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the notification description.
+ /// </summary>
+ public string? Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the notification level.
+ /// </summary>
+ public NotificationLevel? NotificationLevel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the notification url.
+ /// </summary>
+ public string? Url { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index 9edc19bb6..291e571dc 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -11,7 +11,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// <summary>
/// Class TranscodingJob.
/// </summary>
- public class TranscodingJobDto
+ public class TranscodingJobDto : IDisposable
{
/// <summary>
/// The process lock.
@@ -249,5 +249,31 @@ namespace Jellyfin.Api.Models.PlaybackDtos
}
}
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose all resources.
+ /// </summary>
+ /// <param name="disposing">Whether to dispose all resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Process?.Dispose();
+ Process = null;
+ KillTimer?.Dispose();
+ KillTimer = null;
+ CancellationTokenSource?.Dispose();
+ CancellationTokenSource = null;
+ TranscodingThrottler?.Dispose();
+ TranscodingThrottler = null;
+ }
+ }
}
}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
index 872a46824..7b32d76ba 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
@@ -98,7 +98,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
private EncodingOptions GetOptions()
{
- return _config.GetConfiguration<EncodingOptions>("encoding");
+ return _config.GetEncodingOptions();
}
private async void TimerCallback(object? state)
@@ -145,7 +145,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
- var path = job.Path;
+ var path = job.Path ?? throw new ArgumentException("Path can't be null.");
+
var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
index 393627435..41f7b169e 100644
--- a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
+++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs
@@ -1,4 +1,6 @@
-namespace Jellyfin.Api.Models.UserDtos
+using System;
+
+namespace Jellyfin.Api.Models.UserDtos
{
/// <summary>
/// The authenticate user by name request body.
@@ -18,6 +20,7 @@
/// <summary>
/// Gets or sets the sha1-hashed password.
/// </summary>
+ [Obsolete("Send password using pw field")]
public string? Password { get; set; }
}
}
diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs
index 4e75f4cfd..b7ba30180 100644
--- a/Jellyfin.Data/DayOfWeekHelper.cs
+++ b/Jellyfin.Data/DayOfWeekHelper.cs
@@ -1,67 +1,21 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data
{
public static class DayOfWeekHelper
{
- public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
+ public static DayOfWeek[] GetDaysOfWeek(DynamicDayOfWeek day)
{
- var days = new List<DayOfWeek>(7);
-
- if (day == DynamicDayOfWeek.Sunday
- || day == DynamicDayOfWeek.Weekend
- || day == DynamicDayOfWeek.Everyday)
- {
- days.Add(DayOfWeek.Sunday);
- }
-
- if (day == DynamicDayOfWeek.Monday
- || day == DynamicDayOfWeek.Weekday
- || day == DynamicDayOfWeek.Everyday)
- {
- days.Add(DayOfWeek.Monday);
- }
-
- if (day == DynamicDayOfWeek.Tuesday
- || day == DynamicDayOfWeek.Weekday
- || day == DynamicDayOfWeek.Everyday)
- {
- days.Add(DayOfWeek.Tuesday);
- }
-
- if (day == DynamicDayOfWeek.Wednesday
- || day == DynamicDayOfWeek.Weekday
- || day == DynamicDayOfWeek.Everyday)
+ return day switch
{
- days.Add(DayOfWeek.Wednesday);
- }
-
- if (day == DynamicDayOfWeek.Thursday
- || day == DynamicDayOfWeek.Weekday
- || day == DynamicDayOfWeek.Everyday)
- {
- days.Add(DayOfWeek.Thursday);
- }
-
- if (day == DynamicDayOfWeek.Friday
- || day == DynamicDayOfWeek.Weekday
- || day == DynamicDayOfWeek.Everyday)
- {
- days.Add(DayOfWeek.Friday);
- }
-
- if (day == DynamicDayOfWeek.Saturday
- || day == DynamicDayOfWeek.Weekend
- || day == DynamicDayOfWeek.Everyday)
- {
- days.Add(DayOfWeek.Saturday);
- }
-
- return days;
+ DynamicDayOfWeek.Everyday => new[] { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday },
+ DynamicDayOfWeek.Weekday => new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday },
+ DynamicDayOfWeek.Weekend => new[] { DayOfWeek.Sunday, DayOfWeek.Saturday },
+ _ => new[] { (DayOfWeek)day }
+ };
}
}
}
diff --git a/Jellyfin.Data/Entities/AccessSchedule.cs b/Jellyfin.Data/Entities/AccessSchedule.cs
index 7d1b76a3f..befc4ca02 100644
--- a/Jellyfin.Data/Entities/AccessSchedule.cs
+++ b/Jellyfin.Data/Entities/AccessSchedule.cs
@@ -1,7 +1,5 @@
using System;
-using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-using System.Text.Json.Serialization;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
@@ -28,64 +26,37 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Initializes a new instance of the <see cref="AccessSchedule"/> class.
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </summary>
- protected AccessSchedule()
- {
- }
-
- /// <summary>
- /// Gets or sets the id of this instance.
+ /// Gets the id of this instance.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[XmlIgnore]
- [Key]
- [Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
- /// Gets or sets the id of the associated user.
+ /// Gets the id of the associated user.
/// </summary>
[XmlIgnore]
- [Required]
- public Guid UserId { get; protected set; }
+ public Guid UserId { get; private set; }
/// <summary>
/// Gets or sets the day of week.
/// </summary>
/// <value>The day of week.</value>
- [Required]
public DynamicDayOfWeek DayOfWeek { get; set; }
/// <summary>
/// Gets or sets the start hour.
/// </summary>
/// <value>The start hour.</value>
- [Required]
public double StartHour { get; set; }
/// <summary>
/// Gets or sets the end hour.
/// </summary>
/// <value>The end hour.</value>
- [Required]
public double EndHour { get; set; }
-
- /// <summary>
- /// Static create function (for use in LINQ queries, etc.)
- /// </summary>
- /// <param name="dayOfWeek">The day of the week.</param>
- /// <param name="startHour">The start hour.</param>
- /// <param name="endHour">The end hour.</param>
- /// <param name="userId">The associated user's id.</param>
- /// <returns>The newly created instance.</returns>
- public static AccessSchedule Create(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
- {
- return new AccessSchedule(dayOfWeek, startHour, endHour, userId);
- }
}
}
diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs
index e2d5c7187..1d1b86552 100644
--- a/Jellyfin.Data/Entities/ActivityLog.cs
+++ b/Jellyfin.Data/Entities/ActivityLog.cs
@@ -18,8 +18,7 @@ namespace Jellyfin.Data.Entities
/// <param name="name">The name.</param>
/// <param name="type">The type.</param>
/// <param name="userId">The user id.</param>
- /// <param name="logLevel">The log level.</param>
- public ActivityLog(string name, string type, Guid userId, LogLevel logLevel = LogLevel.Information)
+ public ActivityLog(string name, string type, Guid userId)
{
if (string.IsNullOrEmpty(name))
{
@@ -35,23 +34,14 @@ namespace Jellyfin.Data.Entities
Type = type;
UserId = userId;
DateCreated = DateTime.UtcNow;
- LogSeverity = logLevel;
+ LogSeverity = LogLevel.Information;
}
/// <summary>
- /// Initializes a new instance of the <see cref="ActivityLog"/> class.
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </summary>
- protected ActivityLog()
- {
- }
-
- /// <summary>
- /// Gets or sets the identity of this instance.
- /// This is the key in the backing database.
+ /// Gets the identity of this instance.
/// </summary>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -59,7 +49,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 512.
/// </remarks>
- [Required]
[MaxLength(512)]
[StringLength(512)]
public string Name { get; set; }
@@ -72,7 +61,7 @@ namespace Jellyfin.Data.Entities
/// </remarks>
[MaxLength(512)]
[StringLength(512)]
- public string Overview { get; set; }
+ public string? Overview { get; set; }
/// <summary>
/// Gets or sets the short overview.
@@ -82,7 +71,7 @@ namespace Jellyfin.Data.Entities
/// </remarks>
[MaxLength(512)]
[StringLength(512)]
- public string ShortOverview { get; set; }
+ public string? ShortOverview { get; set; }
/// <summary>
/// Gets or sets the type.
@@ -90,7 +79,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 256.
/// </remarks>
- [Required]
[MaxLength(256)]
[StringLength(256)]
public string Type { get; set; }
@@ -111,7 +99,7 @@ namespace Jellyfin.Data.Entities
/// </remarks>
[MaxLength(256)]
[StringLength(256)]
- public string ItemId { get; set; }
+ public string? ItemId { get; set; }
/// <summary>
/// Gets or sets the date created. This should be in UTC.
@@ -131,7 +119,7 @@ namespace Jellyfin.Data.Entities
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs b/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs
index 511e3b281..b07b7c731 100644
--- a/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs
@@ -15,32 +15,25 @@ namespace Jellyfin.Data.Entities
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client.</param>
- /// <param name="preferenceKey">The preference key.</param>
- /// <param name="preferenceValue">The preference value.</param>
- public CustomItemDisplayPreferences(Guid userId, Guid itemId, string client, string preferenceKey, string preferenceValue)
+ /// <param name="key">The preference key.</param>
+ /// <param name="value">The preference value.</param>
+ public CustomItemDisplayPreferences(Guid userId, Guid itemId, string client, string key, string? value)
{
UserId = userId;
ItemId = itemId;
Client = client;
- Key = preferenceKey;
- Value = preferenceValue;
+ Key = key;
+ Value = value;
}
/// <summary>
- /// Initializes a new instance of the <see cref="CustomItemDisplayPreferences"/> class.
- /// </summary>
- protected CustomItemDisplayPreferences()
- {
- }
-
- /// <summary>
- /// Gets or sets the Id.
+ /// Gets the Id.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the user Id.
@@ -64,7 +57,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required. Max Length = 32.
/// </remarks>
- [Required]
[MaxLength(32)]
[StringLength(32)]
public string Client { get; set; }
@@ -75,7 +67,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required.
/// </remarks>
- [Required]
public string Key { get; set; }
/// <summary>
@@ -84,7 +75,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required.
/// </remarks>
- [Required]
- public string Value { get; set; }
+ public string? Value { get; set; }
}
}
diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index 1a8ca1da3..646961238 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -1,6 +1,4 @@
-#pragma warning disable CA2227
-
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -30,27 +28,18 @@ namespace Jellyfin.Data.Entities
SkipBackwardLength = 10000;
ScrollDirection = ScrollDirection.Horizontal;
ChromecastVersion = ChromecastVersion.Stable;
- DashboardTheme = string.Empty;
- TvHome = string.Empty;
HomeSections = new HashSet<HomeSection>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
- /// </summary>
- protected DisplayPreferences()
- {
- }
-
- /// <summary>
- /// Gets or sets the Id.
+ /// Gets the Id.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the user Id.
@@ -74,7 +63,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required. Max Length = 32.
/// </remarks>
- [Required]
[MaxLength(32)]
[StringLength(32)]
public string Client { get; set; }
@@ -145,18 +133,18 @@ namespace Jellyfin.Data.Entities
/// </summary>
[MaxLength(32)]
[StringLength(32)]
- public string DashboardTheme { get; set; }
+ public string? DashboardTheme { get; set; }
/// <summary>
/// Gets or sets the tv home screen.
/// </summary>
[MaxLength(32)]
[StringLength(32)]
- public string TvHome { get; set; }
+ public string? TvHome { get; set; }
/// <summary>
- /// Gets or sets the home sections.
+ /// Gets the home sections.
/// </summary>
- public virtual ICollection<HomeSection> HomeSections { get; protected set; }
+ public virtual ICollection<HomeSection> HomeSections { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs
index 878811e59..14da0bb15 100644
--- a/Jellyfin.Data/Entities/Group.cs
+++ b/Jellyfin.Data/Entities/Group.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -33,22 +31,12 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Initializes a new instance of the <see cref="Group"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Group()
- {
- }
-
- /// <summary>
- /// Gets or sets the id of this group.
+ /// Gets the id of this group.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
- public Guid Id { get; protected set; }
+ public Guid Id { get; private set; }
/// <summary>
/// Gets or sets the group's name.
@@ -56,24 +44,23 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string Name { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the group's permissions.
+ /// Gets a collection containing the group's permissions.
/// </summary>
- public virtual ICollection<Permission> Permissions { get; protected set; }
+ public virtual ICollection<Permission> Permissions { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the group's preferences.
+ /// Gets a collection containing the group's preferences.
/// </summary>
- public virtual ICollection<Preference> Preferences { get; protected set; }
+ public virtual ICollection<Preference> Preferences { get; private set; }
/// <inheritdoc/>
public bool HasPermission(PermissionKind kind)
diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs
index 062046260..e194aa537 100644
--- a/Jellyfin.Data/Entities/HomeSection.cs
+++ b/Jellyfin.Data/Entities/HomeSection.cs
@@ -1,5 +1,4 @@
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
+using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
@@ -10,14 +9,13 @@ namespace Jellyfin.Data.Entities
public class HomeSection
{
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity. Required.
/// </remarks>
- [Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the Id of the associated display preferences.
diff --git a/Jellyfin.Data/Entities/ImageInfo.cs b/Jellyfin.Data/Entities/ImageInfo.cs
index ab8452e62..b5c7a1c12 100644
--- a/Jellyfin.Data/Entities/ImageInfo.cs
+++ b/Jellyfin.Data/Entities/ImageInfo.cs
@@ -20,28 +20,18 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Initializes a new instance of the <see cref="ImageInfo"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected ImageInfo()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
- /// Gets or sets the user id.
+ /// Gets the user id.
/// </summary>
- public Guid? UserId { get; protected set; }
+ public Guid? UserId { get; private set; }
/// <summary>
/// Gets or sets the path of the image.
@@ -49,7 +39,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required.
/// </remarks>
- [Required]
[MaxLength(512)]
[StringLength(512)]
public string Path { get; set; }
diff --git a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
index 2b25bb25f..948126d0a 100644
--- a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
@@ -29,20 +29,13 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
- /// </summary>
- protected ItemDisplayPreferences()
- {
- }
-
- /// <summary>
- /// Gets or sets the Id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the user Id.
@@ -66,7 +59,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required. Max Length = 32.
/// </remarks>
- [Required]
[MaxLength(32)]
[StringLength(32)]
public string Client { get; set; }
@@ -106,7 +98,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required.
/// </remarks>
- [Required]
[MaxLength(64)]
[StringLength(64)]
public string SortBy { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/Artwork.cs b/Jellyfin.Data/Entities/Libraries/Artwork.cs
index 06cd33330..923525fc5 100644
--- a/Jellyfin.Data/Entities/Libraries/Artwork.cs
+++ b/Jellyfin.Data/Entities/Libraries/Artwork.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -18,8 +16,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="path">The path.</param>
/// <param name="kind">The kind of art.</param>
- /// <param name="owner">The owner.</param>
- public Artwork(string path, ArtKind kind, IHasArtwork owner)
+ public Artwork(string path, ArtKind kind)
{
if (string.IsNullOrEmpty(path))
{
@@ -28,28 +25,16 @@ namespace Jellyfin.Data.Entities.Libraries
Path = path;
Kind = kind;
-
- owner?.Artwork.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Artwork"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Artwork()
- {
}
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the path.
@@ -57,7 +42,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 65535.
/// </remarks>
- [Required]
[MaxLength(65535)]
[StringLength(65535)]
public string Path { get; set; }
@@ -72,7 +56,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/Book.cs b/Jellyfin.Data/Entities/Libraries/Book.cs
index 2e63f75bd..a838686d0 100644
--- a/Jellyfin.Data/Entities/Libraries/Book.cs
+++ b/Jellyfin.Data/Entities/Libraries/Book.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System.Collections.Generic;
using Jellyfin.Data.Interfaces;
@@ -13,18 +11,19 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="Book"/> class.
/// </summary>
- public Book()
+ /// <param name="library">The library.</param>
+ public Book(Library library) : base(library)
{
BookMetadata = new HashSet<BookMetadata>();
Releases = new HashSet<Release>();
}
/// <summary>
- /// Gets or sets a collection containing the metadata for this book.
+ /// Gets a collection containing the metadata for this book.
/// </summary>
- public virtual ICollection<BookMetadata> BookMetadata { get; protected set; }
+ public virtual ICollection<BookMetadata> BookMetadata { get; private set; }
/// <inheritdoc />
- public virtual ICollection<Release> Releases { get; protected set; }
+ public virtual ICollection<Release> Releases { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs
index 4a3d290f0..4a350d200 100644
--- a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs
@@ -1,8 +1,4 @@
-#pragma warning disable CA2227
-
-using System;
using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
namespace Jellyfin.Data.Entities.Libraries
@@ -17,41 +13,22 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the object.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="book">The book.</param>
- public BookMetadata(string title, string language, Book book) : base(title, language)
+ public BookMetadata(string title, string language) : base(title, language)
{
- if (book == null)
- {
- throw new ArgumentNullException(nameof(book));
- }
-
- book.BookMetadata.Add(this);
-
Publishers = new HashSet<Company>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="BookMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected BookMetadata()
- {
- }
-
- /// <summary>
/// Gets or sets the ISBN.
/// </summary>
public long? Isbn { get; set; }
/// <summary>
- /// Gets or sets a collection of the publishers for this book.
+ /// Gets a collection of the publishers for this book.
/// </summary>
- public virtual ICollection<Company> Publishers { get; protected set; }
+ public virtual ICollection<Company> Publishers { get; private set; }
/// <inheritdoc />
- [NotMapped]
public ICollection<Company> Companies => Publishers;
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/Chapter.cs b/Jellyfin.Data/Entities/Libraries/Chapter.cs
index f503de379..3d81f713d 100644
--- a/Jellyfin.Data/Entities/Libraries/Chapter.cs
+++ b/Jellyfin.Data/Entities/Libraries/Chapter.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -17,8 +15,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="language">ISO-639-3 3-character language codes.</param>
/// <param name="startTime">The start time for this chapter.</param>
- /// <param name="release">The release.</param>
- public Chapter(string language, long startTime, Release release)
+ public Chapter(string language, long startTime)
{
if (string.IsNullOrEmpty(language))
{
@@ -27,33 +24,16 @@ namespace Jellyfin.Data.Entities.Libraries
Language = language;
StartTime = startTime;
-
- if (release == null)
- {
- throw new ArgumentNullException(nameof(release));
- }
-
- release.Chapters.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Chapter"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Chapter()
- {
}
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -63,7 +43,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the language.
@@ -72,7 +52,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// Required, Min length = 3, Max length = 3
/// ISO-639-3 3-character language codes.
/// </remarks>
- [Required]
[MinLength(3)]
[MaxLength(3)]
[StringLength(3)]
@@ -93,7 +72,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; protected set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/Collection.cs b/Jellyfin.Data/Entities/Libraries/Collection.cs
index 39eded752..7de601969 100644
--- a/Jellyfin.Data/Entities/Libraries/Collection.cs
+++ b/Jellyfin.Data/Entities/Libraries/Collection.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CA2227
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -21,13 +21,13 @@ namespace Jellyfin.Data.Entities.Libraries
}
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -37,16 +37,16 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
- /// Gets or sets a collection containing this collection's items.
+ /// Gets a collection containing this collection's items.
/// </summary>
- public virtual ICollection<CollectionItem> Items { get; protected set; }
+ public virtual ICollection<CollectionItem> Items { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
index f9539964d..0cb4716db 100644
--- a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
+++ b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
@@ -13,39 +12,10 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="CollectionItem"/> class.
/// </summary>
- /// <param name="collection">The collection.</param>
- /// <param name="previous">The previous item.</param>
- /// <param name="next">The next item.</param>
- public CollectionItem(Collection collection, CollectionItem previous, CollectionItem next)
- {
- if (collection == null)
- {
- throw new ArgumentNullException(nameof(collection));
- }
-
- collection.Items.Add(this);
-
- if (next != null)
- {
- Next = next;
- next.Previous = this;
- }
-
- if (previous != null)
- {
- Previous = previous;
- previous.Next = this;
- }
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CollectionItem"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected CollectionItem()
+ /// <param name="libraryItem">The library item.</param>
+ public CollectionItem(LibraryItem libraryItem)
{
+ LibraryItem = libraryItem;
}
/// <summary>
@@ -59,7 +29,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
/// Gets or sets the library item.
@@ -75,7 +45,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// TODO check if this properly updated Dependant and has the proper principal relationship.
/// </remarks>
- public virtual CollectionItem Next { get; set; }
+ public virtual CollectionItem? Next { get; set; }
/// <summary>
/// Gets or sets the previous item in the collection.
@@ -83,7 +53,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// TODO check if this properly updated Dependant and has the proper principal relationship.
/// </remarks>
- public virtual CollectionItem Previous { get; set; }
+ public virtual CollectionItem? Previous { get; set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/Company.cs b/Jellyfin.Data/Entities/Libraries/Company.cs
index 3b6ed3309..1abbee445 100644
--- a/Jellyfin.Data/Entities/Libraries/Company.cs
+++ b/Jellyfin.Data/Entities/Libraries/Company.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -15,49 +13,36 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="Company"/> class.
/// </summary>
- /// <param name="owner">The owner of this company.</param>
- public Company(IHasCompanies owner)
+ public Company()
{
- owner?.Companies.Add(this);
-
CompanyMetadata = new HashSet<CompanyMetadata>();
+ ChildCompanies = new HashSet<Company>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="Company"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Company()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the metadata.
+ /// Gets a collection containing the metadata.
/// </summary>
- public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; }
+ public virtual ICollection<CompanyMetadata> CompanyMetadata { get; private set; }
/// <summary>
- /// Gets or sets a collection containing this company's child companies.
+ /// Gets a collection containing this company's child companies.
/// </summary>
- public virtual ICollection<Company> ChildCompanies { get; protected set; }
+ public virtual ICollection<Company> ChildCompanies { get; private set; }
/// <inheritdoc />
- [NotMapped]
public ICollection<Company> Companies => ChildCompanies;
/// <inheritdoc />
diff --git a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs
index 8aa0486af..a29f08c7f 100644
--- a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Data.Entities.Libraries
@@ -13,21 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the object.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="company">The company.</param>
- public CompanyMetadata(string title, string language, Company company) : base(title, language)
- {
- if (company == null)
- {
- throw new ArgumentNullException(nameof(company));
- }
-
- company.CompanyMetadata.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CompanyMetadata"/> class.
- /// </summary>
- protected CompanyMetadata()
+ public CompanyMetadata(string title, string language) : base(title, language)
{
}
@@ -39,7 +24,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
- public string Description { get; set; }
+ public string? Description { get; set; }
/// <summary>
/// Gets or sets the headquarters.
@@ -49,7 +34,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(255)]
[StringLength(255)]
- public string Headquarters { get; set; }
+ public string? Headquarters { get; set; }
/// <summary>
/// Gets or sets the country code.
@@ -59,7 +44,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(2)]
[StringLength(2)]
- public string Country { get; set; }
+ public string? Country { get; set; }
/// <summary>
/// Gets or sets the homepage.
@@ -69,6 +54,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Homepage { get; set; }
+ public string? Homepage { get; set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/CustomItem.cs b/Jellyfin.Data/Entities/Libraries/CustomItem.cs
index 115489c78..e27d01d86 100644
--- a/Jellyfin.Data/Entities/Libraries/CustomItem.cs
+++ b/Jellyfin.Data/Entities/Libraries/CustomItem.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System.Collections.Generic;
using Jellyfin.Data.Interfaces;
@@ -13,18 +11,19 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="CustomItem"/> class.
/// </summary>
- public CustomItem()
+ /// <param name="library">The library.</param>
+ public CustomItem(Library library) : base(library)
{
CustomItemMetadata = new HashSet<CustomItemMetadata>();
Releases = new HashSet<Release>();
}
/// <summary>
- /// Gets or sets a collection containing the metadata for this item.
+ /// Gets a collection containing the metadata for this item.
/// </summary>
- public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; protected set; }
+ public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; private set; }
/// <inheritdoc />
- public virtual ICollection<Release> Releases { get; protected set; }
+ public virtual ICollection<Release> Releases { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs
index f0daedfbe..af2393870 100644
--- a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace Jellyfin.Data.Entities.Libraries
{
/// <summary>
@@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the object.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="item">The item.</param>
- public CustomItemMetadata(string title, string language, CustomItem item) : base(title, language)
- {
- if (item == null)
- {
- throw new ArgumentNullException(nameof(item));
- }
-
- item.CustomItemMetadata.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CustomItemMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected CustomItemMetadata()
+ public CustomItemMetadata(string title, string language) : base(title, language)
{
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/Episode.cs b/Jellyfin.Data/Entities/Libraries/Episode.cs
index 0bdc2d764..ce2f0c617 100644
--- a/Jellyfin.Data/Entities/Libraries/Episode.cs
+++ b/Jellyfin.Data/Entities/Libraries/Episode.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CA2227
-
-using System;
using System.Collections.Generic;
using Jellyfin.Data.Interfaces;
@@ -14,41 +11,24 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="Episode"/> class.
/// </summary>
- /// <param name="season">The season.</param>
- public Episode(Season season)
+ /// <param name="library">The library.</param>
+ public Episode(Library library) : base(library)
{
- if (season == null)
- {
- throw new ArgumentNullException(nameof(season));
- }
-
- season.Episodes.Add(this);
-
Releases = new HashSet<Release>();
EpisodeMetadata = new HashSet<EpisodeMetadata>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="Episode"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Episode()
- {
- }
-
- /// <summary>
/// Gets or sets the episode number.
/// </summary>
public int? EpisodeNumber { get; set; }
/// <inheritdoc />
- public virtual ICollection<Release> Releases { get; protected set; }
+ public virtual ICollection<Release> Releases { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the metadata for this episode.
+ /// Gets a collection containing the metadata for this episode.
/// </summary>
- public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; protected set; }
+ public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs
index 7efb840f0..b0ef11e0f 100644
--- a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Data.Entities.Libraries
@@ -13,24 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the object.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="episode">The episode.</param>
- public EpisodeMetadata(string title, string language, Episode episode) : base(title, language)
- {
- if (episode == null)
- {
- throw new ArgumentNullException(nameof(episode));
- }
-
- episode.EpisodeMetadata.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EpisodeMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected EpisodeMetadata()
+ public EpisodeMetadata(string title, string language) : base(title, language)
{
}
@@ -42,7 +24,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Outline { get; set; }
+ public string? Outline { get; set; }
/// <summary>
/// Gets or sets the plot.
@@ -52,7 +34,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
- public string Plot { get; set; }
+ public string? Plot { get; set; }
/// <summary>
/// Gets or sets the tagline.
@@ -62,6 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Tagline { get; set; }
+ public string? Tagline { get; set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/Genre.cs b/Jellyfin.Data/Entities/Libraries/Genre.cs
index 2a2dbd1a5..3b822ee82 100644
--- a/Jellyfin.Data/Entities/Libraries/Genre.cs
+++ b/Jellyfin.Data/Entities/Libraries/Genre.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
@@ -14,42 +13,19 @@ namespace Jellyfin.Data.Entities.Libraries
/// Initializes a new instance of the <see cref="Genre"/> class.
/// </summary>
/// <param name="name">The name.</param>
- /// <param name="itemMetadata">The metadata.</param>
- public Genre(string name, ItemMetadata itemMetadata)
+ public Genre(string name)
{
- if (string.IsNullOrEmpty(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
-
Name = name;
-
- if (itemMetadata == null)
- {
- throw new ArgumentNullException(nameof(itemMetadata));
- }
-
- itemMetadata.Genres.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Genre"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Genre()
- {
}
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -57,14 +33,13 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Indexed, Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string Name { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; protected set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
index d74330c05..d429a90c6 100644
--- a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -43,23 +41,13 @@ namespace Jellyfin.Data.Entities.Libraries
}
/// <summary>
- /// Initializes a new instance of the <see cref="ItemMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to being abstract.
- /// </remarks>
- protected ItemMetadata()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the title.
@@ -67,7 +55,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 1024.
/// </remarks>
- [Required]
[MaxLength(1024)]
[StringLength(1024)]
public string Title { get; set; }
@@ -80,7 +67,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string OriginalTitle { get; set; }
+ public string? OriginalTitle { get; set; }
/// <summary>
/// Gets or sets the sort title.
@@ -90,7 +77,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string SortTitle { get; set; }
+ public string? SortTitle { get; set; }
/// <summary>
/// Gets or sets the language.
@@ -99,7 +86,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// Required, Min length = 3, Max length = 3.
/// ISO-639-3 3-character language codes.
/// </remarks>
- [Required]
[MinLength(3)]
[MaxLength(3)]
[StringLength(3)]
@@ -111,12 +97,12 @@ namespace Jellyfin.Data.Entities.Libraries
public DateTimeOffset? ReleaseDate { get; set; }
/// <summary>
- /// Gets or sets the date added.
+ /// Gets the date added.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
- public DateTime DateAdded { get; protected set; }
+ public DateTime DateAdded { get; private set; }
/// <summary>
/// Gets or sets the date modified.
@@ -126,37 +112,32 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
public DateTime DateModified { get; set; }
- /// <summary>
- /// Gets or sets the row version.
- /// </summary>
- /// <remarks>
- /// Required, ConcurrencyToken.
- /// </remarks>
+ /// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the person roles for this item.
+ /// Gets a collection containing the person roles for this item.
/// </summary>
- public virtual ICollection<PersonRole> PersonRoles { get; protected set; }
+ public virtual ICollection<PersonRole> PersonRoles { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the genres for this item.
+ /// Gets a collection containing the genres for this item.
/// </summary>
- public virtual ICollection<Genre> Genres { get; protected set; }
+ public virtual ICollection<Genre> Genres { get; private set; }
/// <inheritdoc />
- public virtual ICollection<Artwork> Artwork { get; protected set; }
+ public virtual ICollection<Artwork> Artwork { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the ratings for this item.
+ /// Gets a collection containing the ratings for this item.
/// </summary>
- public virtual ICollection<Rating> Ratings { get; protected set; }
+ public virtual ICollection<Rating> Ratings { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the metadata sources for this item.
+ /// Gets a collection containing the metadata sources for this item.
/// </summary>
- public virtual ICollection<MetadataProviderId> Sources { get; protected set; }
+ public virtual ICollection<MetadataProviderId> Sources { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/Library.cs b/Jellyfin.Data/Entities/Libraries/Library.cs
index 4f82a2e2a..0db42a1c7 100644
--- a/Jellyfin.Data/Entities/Libraries/Library.cs
+++ b/Jellyfin.Data/Entities/Libraries/Library.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
@@ -14,34 +13,21 @@ namespace Jellyfin.Data.Entities.Libraries
/// Initializes a new instance of the <see cref="Library"/> class.
/// </summary>
/// <param name="name">The name of the library.</param>
- public Library(string name)
+ /// <param name="path">The path of the library.</param>
+ public Library(string name, string path)
{
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
-
Name = name;
+ Path = path;
}
/// <summary>
- /// Initializes a new instance of the <see cref="Library"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Library()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -49,7 +35,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 128.
/// </remarks>
- [Required]
[MaxLength(128)]
[StringLength(128)]
public string Name { get; set; }
@@ -60,12 +45,11 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required.
/// </remarks>
- [Required]
public string Path { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs
index a9167aa7f..d889b871e 100644
--- a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs
+++ b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs
@@ -21,29 +21,22 @@ namespace Jellyfin.Data.Entities.Libraries
}
/// <summary>
- /// Initializes a new instance of the <see cref="LibraryItem"/> class.
- /// </summary>
- protected LibraryItem()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
- /// Gets or sets the date this library item was added.
+ /// Gets the date this library item was added.
/// </summary>
- public DateTime DateAdded { get; protected set; }
+ public DateTime DateAdded { get; private set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; protected set; }
+ public uint RowVersion { get; private set; }
/// <summary>
/// Gets or sets the library of this item.
@@ -51,7 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required.
/// </remarks>
- [Required]
public virtual Library Library { get; set; }
/// <inheritdoc />
diff --git a/Jellyfin.Data/Entities/Libraries/MediaFile.cs b/Jellyfin.Data/Entities/Libraries/MediaFile.cs
index 9924d5728..36e1e4777 100644
--- a/Jellyfin.Data/Entities/Libraries/MediaFile.cs
+++ b/Jellyfin.Data/Entities/Libraries/MediaFile.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -19,8 +17,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="path">The path relative to the LibraryRoot.</param>
/// <param name="kind">The file kind.</param>
- /// <param name="release">The release.</param>
- public MediaFile(string path, MediaFileKind kind, Release release)
+ public MediaFile(string path, MediaFileKind kind)
{
if (string.IsNullOrEmpty(path))
{
@@ -30,34 +27,17 @@ namespace Jellyfin.Data.Entities.Libraries
Path = path;
Kind = kind;
- if (release == null)
- {
- throw new ArgumentNullException(nameof(release));
- }
-
- release.MediaFiles.Add(this);
-
MediaFileStreams = new HashSet<MediaFileStream>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="MediaFile"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected MediaFile()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the path relative to the library root.
@@ -65,7 +45,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 65535.
/// </remarks>
- [Required]
[MaxLength(65535)]
[StringLength(65535)]
public string Path { get; set; }
@@ -80,12 +59,12 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the streams in this file.
+ /// Gets a collection containing the streams in this file.
/// </summary>
- public virtual ICollection<MediaFileStream> MediaFileStreams { get; protected set; }
+ public virtual ICollection<MediaFileStream> MediaFileStreams { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs
index 5b03e260e..e24e73ecb 100644
--- a/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs
+++ b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs
@@ -1,4 +1,5 @@
-using System;
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
@@ -14,37 +15,19 @@ namespace Jellyfin.Data.Entities.Libraries
/// Initializes a new instance of the <see cref="MediaFileStream"/> class.
/// </summary>
/// <param name="streamNumber">The number of this stream.</param>
- /// <param name="mediaFile">The media file.</param>
- public MediaFileStream(int streamNumber, MediaFile mediaFile)
+ public MediaFileStream(int streamNumber)
{
StreamNumber = streamNumber;
-
- if (mediaFile == null)
- {
- throw new ArgumentNullException(nameof(mediaFile));
- }
-
- mediaFile.MediaFileStreams.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MediaFileStream"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected MediaFileStream()
- {
}
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the stream number.
@@ -56,7 +39,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs
index a18a612bc..b27196078 100644
--- a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs
+++ b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs
@@ -25,23 +25,13 @@ namespace Jellyfin.Data.Entities.Libraries
}
/// <summary>
- /// Initializes a new instance of the <see cref="MetadataProvider"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected MetadataProvider()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -49,14 +39,13 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 1024.
/// </remarks>
- [Required]
[MaxLength(1024)]
[StringLength(1024)]
public string Name { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs
index fcfb35bfa..44c198518 100644
--- a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs
+++ b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs
@@ -14,8 +14,8 @@ namespace Jellyfin.Data.Entities.Libraries
/// Initializes a new instance of the <see cref="MetadataProviderId"/> class.
/// </summary>
/// <param name="providerId">The provider id.</param>
- /// <param name="itemMetadata">The metadata entity.</param>
- public MetadataProviderId(string providerId, ItemMetadata itemMetadata)
+ /// <param name="metadataProvider">The metadata provider.</param>
+ public MetadataProviderId(string providerId, MetadataProvider metadataProvider)
{
if (string.IsNullOrEmpty(providerId))
{
@@ -23,33 +23,17 @@ namespace Jellyfin.Data.Entities.Libraries
}
ProviderId = providerId;
-
- if (itemMetadata == null)
- {
- throw new ArgumentNullException(nameof(itemMetadata));
- }
-
- itemMetadata.Sources.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MetadataProviderId"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected MetadataProviderId()
- {
+ MetadataProvider = metadataProvider;
}
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the provider id.
@@ -57,14 +41,13 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string ProviderId { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
/// Gets or sets the metadata provider.
diff --git a/Jellyfin.Data/Entities/Libraries/Movie.cs b/Jellyfin.Data/Entities/Libraries/Movie.cs
index 08db904fa..499fafd0e 100644
--- a/Jellyfin.Data/Entities/Libraries/Movie.cs
+++ b/Jellyfin.Data/Entities/Libraries/Movie.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System.Collections.Generic;
using Jellyfin.Data.Interfaces;
@@ -13,18 +11,19 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="Movie"/> class.
/// </summary>
- public Movie()
+ /// <param name="library">The library.</param>
+ public Movie(Library library) : base(library)
{
Releases = new HashSet<Release>();
MovieMetadata = new HashSet<MovieMetadata>();
}
/// <inheritdoc />
- public virtual ICollection<Release> Releases { get; protected set; }
+ public virtual ICollection<Release> Releases { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the metadata for this movie.
+ /// Gets a collection containing the metadata for this movie.
/// </summary>
- public virtual ICollection<MovieMetadata> MovieMetadata { get; protected set; }
+ public virtual ICollection<MovieMetadata> MovieMetadata { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs
index aa1501a5c..44b5f34d7 100644
--- a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs
@@ -1,8 +1,5 @@
-#pragma warning disable CA2227
-
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
namespace Jellyfin.Data.Entities.Libraries
@@ -17,22 +14,9 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the movie.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="movie">The movie.</param>
- public MovieMetadata(string title, string language, Movie movie) : base(title, language)
+ public MovieMetadata(string title, string language) : base(title, language)
{
Studios = new HashSet<Company>();
-
- movie.MovieMetadata.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MovieMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected MovieMetadata()
- {
}
/// <summary>
@@ -43,7 +27,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Outline { get; set; }
+ public string? Outline { get; set; }
/// <summary>
/// Gets or sets the tagline.
@@ -53,7 +37,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Tagline { get; set; }
+ public string? Tagline { get; set; }
/// <summary>
/// Gets or sets the plot.
@@ -63,7 +47,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
- public string Plot { get; set; }
+ public string? Plot { get; set; }
/// <summary>
/// Gets or sets the country code.
@@ -73,15 +57,14 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(2)]
[StringLength(2)]
- public string Country { get; set; }
+ public string? Country { get; set; }
/// <summary>
- /// Gets or sets the studios that produced this movie.
+ /// Gets the studios that produced this movie.
/// </summary>
- public virtual ICollection<Company> Studios { get; protected set; }
+ public virtual ICollection<Company> Studios { get; private set; }
/// <inheritdoc />
- [NotMapped]
public ICollection<Company> Companies => Studios;
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs
index 06aff6f45..d6231bbf0 100644
--- a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs
+++ b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System.Collections.Generic;
namespace Jellyfin.Data.Entities.Libraries
@@ -12,20 +10,21 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="MusicAlbum"/> class.
/// </summary>
- public MusicAlbum()
+ /// <param name="library">The library.</param>
+ public MusicAlbum(Library library) : base(library)
{
MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>();
Tracks = new HashSet<Track>();
}
/// <summary>
- /// Gets or sets a collection containing the album metadata.
+ /// Gets a collection containing the album metadata.
/// </summary>
- public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; protected set; }
+ public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the tracks.
+ /// Gets a collection containing the tracks.
/// </summary>
- public virtual ICollection<Track> Tracks { get; protected set; }
+ public virtual ICollection<Track> Tracks { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs
index 05c0b0374..691f3504f 100644
--- a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -15,22 +13,9 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the album.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="album">The music album.</param>
- public MusicAlbumMetadata(string title, string language, MusicAlbum album) : base(title, language)
+ public MusicAlbumMetadata(string title, string language) : base(title, language)
{
Labels = new HashSet<Company>();
-
- album.MusicAlbumMetadata.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MusicAlbumMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected MusicAlbumMetadata()
- {
}
/// <summary>
@@ -41,7 +26,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(255)]
[StringLength(255)]
- public string Barcode { get; set; }
+ public string? Barcode { get; set; }
/// <summary>
/// Gets or sets the label number.
@@ -51,7 +36,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(255)]
[StringLength(255)]
- public string LabelNumber { get; set; }
+ public string? LabelNumber { get; set; }
/// <summary>
/// Gets or sets the country code.
@@ -61,11 +46,11 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(2)]
[StringLength(2)]
- public string Country { get; set; }
+ public string? Country { get; set; }
/// <summary>
- /// Gets or sets a collection containing the labels.
+ /// Gets a collection containing the labels.
/// </summary>
- public virtual ICollection<Company> Labels { get; protected set; }
+ public virtual ICollection<Company> Labels { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/Person.cs b/Jellyfin.Data/Entities/Libraries/Person.cs
index af4c87b73..8b67d920d 100644
--- a/Jellyfin.Data/Entities/Libraries/Person.cs
+++ b/Jellyfin.Data/Entities/Libraries/Person.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -32,23 +30,13 @@ namespace Jellyfin.Data.Entities.Libraries
}
/// <summary>
- /// Initializes a new instance of the <see cref="Person"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Person()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -56,7 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 1024.
/// </remarks>
- [Required]
[MaxLength(1024)]
[StringLength(1024)]
public string Name { get; set; }
@@ -69,15 +56,15 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(256)]
[StringLength(256)]
- public string SourceId { get; set; }
+ public string? SourceId { get; set; }
/// <summary>
- /// Gets or sets the date added.
+ /// Gets the date added.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
- public DateTime DateAdded { get; protected set; }
+ public DateTime DateAdded { get; private set; }
/// <summary>
/// Gets or sets the date modified.
@@ -89,12 +76,12 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
- /// Gets or sets a list of metadata sources for this person.
+ /// Gets a list of metadata sources for this person.
/// </summary>
- public virtual ICollection<MetadataProviderId> Sources { get; protected set; }
+ public virtual ICollection<MetadataProviderId> Sources { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/PersonRole.cs b/Jellyfin.Data/Entities/Libraries/PersonRole.cs
index cd38ee83d..7d40bdf44 100644
--- a/Jellyfin.Data/Entities/Libraries/PersonRole.cs
+++ b/Jellyfin.Data/Entities/Libraries/PersonRole.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CA2227
-
-using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -18,39 +15,23 @@ namespace Jellyfin.Data.Entities.Libraries
/// Initializes a new instance of the <see cref="PersonRole"/> class.
/// </summary>
/// <param name="type">The role type.</param>
- /// <param name="itemMetadata">The metadata.</param>
- public PersonRole(PersonRoleType type, ItemMetadata itemMetadata)
+ /// <param name="person">The person.</param>
+ public PersonRole(PersonRoleType type, Person person)
{
Type = type;
-
- if (itemMetadata == null)
- {
- throw new ArgumentNullException(nameof(itemMetadata));
- }
-
- itemMetadata.PersonRoles.Add(this);
-
+ Person = person;
+ Artwork = new HashSet<Artwork>();
Sources = new HashSet<MetadataProviderId>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="PersonRole"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected PersonRole()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name of the person's role.
@@ -60,7 +41,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Role { get; set; }
+ public string? Role { get; set; }
/// <summary>
/// Gets or sets the person's role type.
@@ -72,7 +53,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; protected set; }
+ public uint RowVersion { get; private set; }
/// <summary>
/// Gets or sets the person.
@@ -80,16 +61,15 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required.
/// </remarks>
- [Required]
public virtual Person Person { get; set; }
/// <inheritdoc />
- public virtual ICollection<Artwork> Artwork { get; protected set; }
+ public virtual ICollection<Artwork> Artwork { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the metadata sources for this person role.
+ /// Gets a collection containing the metadata sources for this person role.
/// </summary>
- public virtual ICollection<MetadataProviderId> Sources { get; protected set; }
+ public virtual ICollection<MetadataProviderId> Sources { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/Photo.cs b/Jellyfin.Data/Entities/Libraries/Photo.cs
index 25562ec96..4b459432b 100644
--- a/Jellyfin.Data/Entities/Libraries/Photo.cs
+++ b/Jellyfin.Data/Entities/Libraries/Photo.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System.Collections.Generic;
using Jellyfin.Data.Interfaces;
@@ -13,18 +11,19 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="Photo"/> class.
/// </summary>
- public Photo()
+ /// <param name="library">The library.</param>
+ public Photo(Library library) : base(library)
{
PhotoMetadata = new HashSet<PhotoMetadata>();
Releases = new HashSet<Release>();
}
/// <summary>
- /// Gets or sets a collection containing the photo metadata.
+ /// Gets a collection containing the photo metadata.
/// </summary>
- public virtual ICollection<PhotoMetadata> PhotoMetadata { get; protected set; }
+ public virtual ICollection<PhotoMetadata> PhotoMetadata { get; private set; }
/// <inheritdoc />
- public virtual ICollection<Release> Releases { get; protected set; }
+ public virtual ICollection<Release> Releases { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs
index ffc790b57..6c284307d 100644
--- a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace Jellyfin.Data.Entities.Libraries
{
/// <summary>
@@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the photo.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="photo">The photo.</param>
- public PhotoMetadata(string title, string language, Photo photo) : base(title, language)
- {
- if (photo == null)
- {
- throw new ArgumentNullException(nameof(photo));
- }
-
- photo.PhotoMetadata.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PhotoMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected PhotoMetadata()
+ public PhotoMetadata(string title, string language) : base(title, language)
{
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/Rating.cs b/Jellyfin.Data/Entities/Libraries/Rating.cs
index 98226cd80..58c8fa49e 100644
--- a/Jellyfin.Data/Entities/Libraries/Rating.cs
+++ b/Jellyfin.Data/Entities/Libraries/Rating.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
@@ -14,37 +13,19 @@ namespace Jellyfin.Data.Entities.Libraries
/// Initializes a new instance of the <see cref="Rating"/> class.
/// </summary>
/// <param name="value">The value.</param>
- /// <param name="itemMetadata">The metadata.</param>
- public Rating(double value, ItemMetadata itemMetadata)
+ public Rating(double value)
{
Value = value;
-
- if (itemMetadata == null)
- {
- throw new ArgumentNullException(nameof(itemMetadata));
- }
-
- itemMetadata.Ratings.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Rating"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Rating()
- {
}
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the value.
@@ -61,13 +42,13 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
/// Gets or sets the rating type.
/// If this is <c>null</c> it's the internal user rating.
/// </summary>
- public virtual RatingSource RatingType { get; set; }
+ public virtual RatingSource? RatingType { get; set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/RatingSource.cs b/Jellyfin.Data/Entities/Libraries/RatingSource.cs
index 549f41804..0f3a07324 100644
--- a/Jellyfin.Data/Entities/Libraries/RatingSource.cs
+++ b/Jellyfin.Data/Entities/Libraries/RatingSource.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
@@ -15,38 +14,20 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="minimumValue">The minimum value.</param>
/// <param name="maximumValue">The maximum value.</param>
- /// <param name="rating">The rating.</param>
- public RatingSource(double minimumValue, double maximumValue, Rating rating)
+ public RatingSource(double minimumValue, double maximumValue)
{
MinimumValue = minimumValue;
MaximumValue = maximumValue;
-
- if (rating == null)
- {
- throw new ArgumentNullException(nameof(rating));
- }
-
- rating.RatingType = this;
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RatingSource"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected RatingSource()
- {
}
/// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -56,7 +37,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the minimum value.
@@ -76,12 +57,12 @@ namespace Jellyfin.Data.Entities.Libraries
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
/// Gets or sets the metadata source.
/// </summary>
- public virtual MetadataProviderId Source { get; set; }
+ public virtual MetadataProviderId? Source { get; set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/Release.cs b/Jellyfin.Data/Entities/Libraries/Release.cs
index b633e08fb..d3d52bf5c 100644
--- a/Jellyfin.Data/Entities/Libraries/Release.cs
+++ b/Jellyfin.Data/Entities/Libraries/Release.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -17,8 +15,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Initializes a new instance of the <see cref="Release"/> class.
/// </summary>
/// <param name="name">The name of this release.</param>
- /// <param name="owner">The owner of this release.</param>
- public Release(string name, IHasReleases owner)
+ public Release(string name)
{
if (string.IsNullOrEmpty(name))
{
@@ -27,30 +24,18 @@ namespace Jellyfin.Data.Entities.Libraries
Name = name;
- owner?.Releases.Add(this);
-
MediaFiles = new HashSet<MediaFile>();
Chapters = new HashSet<Chapter>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="Release"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Release()
- {
- }
-
- /// <summary>
- /// Gets or sets the id.
+ /// Gets the id.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
/// <summary>
/// Gets or sets the name.
@@ -58,24 +43,23 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 1024.
/// </remarks>
- [Required]
[MaxLength(1024)]
[StringLength(1024)]
public string Name { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the media files for this release.
+ /// Gets a collection containing the media files for this release.
/// </summary>
- public virtual ICollection<MediaFile> MediaFiles { get; protected set; }
+ public virtual ICollection<MediaFile> MediaFiles { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the chapters for this release.
+ /// Gets a collection containing the chapters for this release.
/// </summary>
- public virtual ICollection<Chapter> Chapters { get; protected set; }
+ public virtual ICollection<Chapter> Chapters { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Libraries/Season.cs b/Jellyfin.Data/Entities/Libraries/Season.cs
index eb6674dbc..fc110b49d 100644
--- a/Jellyfin.Data/Entities/Libraries/Season.cs
+++ b/Jellyfin.Data/Entities/Libraries/Season.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CA2227
-
-using System;
using System.Collections.Generic;
namespace Jellyfin.Data.Entities.Libraries
@@ -13,43 +10,26 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="Season"/> class.
/// </summary>
- /// <param name="series">The series.</param>
- public Season(Series series)
+ /// <param name="library">The library.</param>
+ public Season(Library library) : base(library)
{
- if (series == null)
- {
- throw new ArgumentNullException(nameof(series));
- }
-
- series.Seasons.Add(this);
-
Episodes = new HashSet<Episode>();
SeasonMetadata = new HashSet<SeasonMetadata>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="Season"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Season()
- {
- }
-
- /// <summary>
/// Gets or sets the season number.
/// </summary>
public int? SeasonNumber { get; set; }
/// <summary>
- /// Gets or sets the season metadata.
+ /// Gets the season metadata.
/// </summary>
- public virtual ICollection<SeasonMetadata> SeasonMetadata { get; protected set; }
+ public virtual ICollection<SeasonMetadata> SeasonMetadata { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the number of episodes.
+ /// Gets a collection containing the number of episodes.
/// </summary>
- public virtual ICollection<Episode> Episodes { get; protected set; }
+ public virtual ICollection<Episode> Episodes { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs
index 7ce79756b..da40a075f 100644
--- a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs
@@ -1,4 +1,3 @@
-using System;
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Data.Entities.Libraries
@@ -13,24 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the object.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="season">The season.</param>
- public SeasonMetadata(string title, string language, Season season) : base(title, language)
- {
- if (season == null)
- {
- throw new ArgumentNullException(nameof(season));
- }
-
- season.SeasonMetadata.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SeasonMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected SeasonMetadata()
+ public SeasonMetadata(string title, string language) : base(title, language)
{
}
@@ -42,6 +24,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Outline { get; set; }
+ public string? Outline { get; set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/Series.cs b/Jellyfin.Data/Entities/Libraries/Series.cs
index 8c8317d14..0354433e0 100644
--- a/Jellyfin.Data/Entities/Libraries/Series.cs
+++ b/Jellyfin.Data/Entities/Libraries/Series.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.Collections.Generic;
@@ -13,9 +11,9 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="Series"/> class.
/// </summary>
- public Series()
+ /// <param name="library">The library.</param>
+ public Series(Library library) : base(library)
{
- DateAdded = DateTime.UtcNow;
Seasons = new HashSet<Season>();
SeriesMetadata = new HashSet<SeriesMetadata>();
}
@@ -36,13 +34,13 @@ namespace Jellyfin.Data.Entities.Libraries
public DateTime? FirstAired { get; set; }
/// <summary>
- /// Gets or sets a collection containing the series metadata.
+ /// Gets a collection containing the series metadata.
/// </summary>
- public virtual ICollection<SeriesMetadata> SeriesMetadata { get; protected set; }
+ public virtual ICollection<SeriesMetadata> SeriesMetadata { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the seasons.
+ /// Gets a collection containing the seasons.
/// </summary>
- public virtual ICollection<Season> Seasons { get; protected set; }
+ public virtual ICollection<Season> Seasons { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs
index 877dbfc69..42115802c 100644
--- a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs
@@ -1,9 +1,5 @@
-#pragma warning disable CA2227
-
-using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
namespace Jellyfin.Data.Entities.Libraries
@@ -18,30 +14,12 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the object.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="series">The series.</param>
- public SeriesMetadata(string title, string language, Series series) : base(title, language)
+ public SeriesMetadata(string title, string language) : base(title, language)
{
- if (series == null)
- {
- throw new ArgumentNullException(nameof(series));
- }
-
- series.SeriesMetadata.Add(this);
-
Networks = new HashSet<Company>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="SeriesMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected SeriesMetadata()
- {
- }
-
- /// <summary>
/// Gets or sets the outline.
/// </summary>
/// <remarks>
@@ -49,7 +27,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Outline { get; set; }
+ public string? Outline { get; set; }
/// <summary>
/// Gets or sets the plot.
@@ -59,7 +37,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
- public string Plot { get; set; }
+ public string? Plot { get; set; }
/// <summary>
/// Gets or sets the tagline.
@@ -69,7 +47,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Tagline { get; set; }
+ public string? Tagline { get; set; }
/// <summary>
/// Gets or sets the country code.
@@ -79,15 +57,14 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(2)]
[StringLength(2)]
- public string Country { get; set; }
+ public string? Country { get; set; }
/// <summary>
- /// Gets or sets a collection containing the networks.
+ /// Gets a collection containing the networks.
/// </summary>
- public virtual ICollection<Company> Networks { get; protected set; }
+ public virtual ICollection<Company> Networks { get; private set; }
/// <inheritdoc />
- [NotMapped]
public ICollection<Company> Companies => Networks;
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/Track.cs b/Jellyfin.Data/Entities/Libraries/Track.cs
index 782bfb5ce..d35400033 100644
--- a/Jellyfin.Data/Entities/Libraries/Track.cs
+++ b/Jellyfin.Data/Entities/Libraries/Track.cs
@@ -1,6 +1,3 @@
-#pragma warning disable CA2227
-
-using System;
using System.Collections.Generic;
using Jellyfin.Data.Interfaces;
@@ -14,41 +11,24 @@ namespace Jellyfin.Data.Entities.Libraries
/// <summary>
/// Initializes a new instance of the <see cref="Track"/> class.
/// </summary>
- /// <param name="album">The album.</param>
- public Track(MusicAlbum album)
+ /// <param name="library">The library.</param>
+ public Track(Library library) : base(library)
{
- if (album == null)
- {
- throw new ArgumentNullException(nameof(album));
- }
-
- album.Tracks.Add(this);
-
Releases = new HashSet<Release>();
TrackMetadata = new HashSet<TrackMetadata>();
}
/// <summary>
- /// Initializes a new instance of the <see cref="Track"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected Track()
- {
- }
-
- /// <summary>
/// Gets or sets the track number.
/// </summary>
public int? TrackNumber { get; set; }
/// <inheritdoc />
- public virtual ICollection<Release> Releases { get; protected set; }
+ public virtual ICollection<Release> Releases { get; private set; }
/// <summary>
- /// Gets or sets a collection containing the track metadata.
+ /// Gets a collection containing the track metadata.
/// </summary>
- public virtual ICollection<TrackMetadata> TrackMetadata { get; protected set; }
+ public virtual ICollection<TrackMetadata> TrackMetadata { get; private set; }
}
}
diff --git a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs
index 321f93bf2..042d2b90d 100644
--- a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace Jellyfin.Data.Entities.Libraries
{
/// <summary>
@@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </summary>
/// <param name="title">The title or name of the object.</param>
/// <param name="language">ISO-639-3 3-character language codes.</param>
- /// <param name="track">The track.</param>
- public TrackMetadata(string title, string language, Track track) : base(title, language)
- {
- if (track == null)
- {
- throw new ArgumentNullException(nameof(track));
- }
-
- track.TrackMetadata.Add(this);
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TrackMetadata"/> class.
- /// </summary>
- /// <remarks>
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </remarks>
- protected TrackMetadata()
+ public TrackMetadata(string title, string language) : base(title, language)
{
}
}
diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs
index d92e5d9d2..6d2e68077 100644
--- a/Jellyfin.Data/Entities/Permission.cs
+++ b/Jellyfin.Data/Entities/Permission.cs
@@ -1,3 +1,6 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
+using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Enums;
@@ -23,29 +26,26 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Initializes a new instance of the <see cref="Permission"/> class.
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </summary>
- protected Permission()
- {
- }
-
- /// <summary>
- /// Gets or sets the id of this permission.
+ /// Gets the id of this permission.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated user.
+ /// </summary>
+ public Guid? UserId { get; set; }
/// <summary>
- /// Gets or sets the type of this permission.
+ /// Gets the type of this permission.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
- public PermissionKind Kind { get; protected set; }
+ public PermissionKind Kind { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the associated user has this permission.
@@ -57,7 +57,7 @@ namespace Jellyfin.Data.Entities
/// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc/>
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs
index 4efddf2a4..a6ab275d3 100644
--- a/Jellyfin.Data/Entities/Preference.cs
+++ b/Jellyfin.Data/Entities/Preference.cs
@@ -24,29 +24,26 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Initializes a new instance of the <see cref="Preference"/> class.
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </summary>
- protected Preference()
- {
- }
-
- /// <summary>
- /// Gets or sets the id of this preference.
+ /// Gets the id of this preference.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; protected set; }
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated user.
+ /// </summary>
+ public Guid? UserId { get; set; }
/// <summary>
- /// Gets or sets the type of this preference.
+ /// Gets the type of this preference.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
- public PreferenceKind Kind { get; protected set; }
+ public PreferenceKind Kind { get; private set; }
/// <summary>
/// Gets or sets the value of this preference.
@@ -54,14 +51,13 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 65535.
/// </remarks>
- [Required]
[MaxLength(65535)]
[StringLength(65535)]
public string Value { get; set; }
/// <inheritdoc/>
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <inheritdoc/>
public void OnSavingChanges()
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 362f3b4eb..e309e54de 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA2227
-
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -51,6 +49,7 @@ namespace Jellyfin.Data.Entities
PasswordResetProviderId = passwordResetProviderId;
AccessSchedules = new HashSet<AccessSchedule>();
+ DisplayPreferences = new HashSet<DisplayPreferences>();
ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
// Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
@@ -72,17 +71,6 @@ namespace Jellyfin.Data.Entities
PlayDefaultAudioTrack = true;
SubtitleMode = SubtitlePlaybackMode.Default;
SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
-
- AddDefaultPermissions();
- AddDefaultPreferences();
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="User"/> class.
- /// Default constructor. Protected due to required properties, but present because EF needs it.
- /// </summary>
- protected User()
- {
}
/// <summary>
@@ -100,7 +88,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string Username { get; set; }
@@ -113,7 +100,7 @@ namespace Jellyfin.Data.Entities
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
- public string Password { get; set; }
+ public string? Password { get; set; }
/// <summary>
/// Gets or sets the user's easy password, or <c>null</c> if none is set.
@@ -123,7 +110,7 @@ namespace Jellyfin.Data.Entities
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
- public string EasyPassword { get; set; }
+ public string? EasyPassword { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user must update their password.
@@ -141,7 +128,7 @@ namespace Jellyfin.Data.Entities
/// </remarks>
[MaxLength(255)]
[StringLength(255)]
- public string AudioLanguagePreference { get; set; }
+ public string? AudioLanguagePreference { get; set; }
/// <summary>
/// Gets or sets the authentication provider id.
@@ -149,7 +136,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string AuthenticationProviderId { get; set; }
@@ -160,7 +146,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string PasswordResetProviderId { get; set; }
@@ -217,7 +202,7 @@ namespace Jellyfin.Data.Entities
/// </remarks>
[MaxLength(255)]
[StringLength(255)]
- public string SubtitleLanguagePreference { get; set; }
+ public string? SubtitleLanguagePreference { get; set; }
/// <summary>
/// Gets or sets a value indicating whether missing episodes should be displayed.
@@ -312,68 +297,57 @@ namespace Jellyfin.Data.Entities
/// Gets or sets the user's profile image. Can be <c>null</c>.
/// </summary>
// [ForeignKey("UserId")]
- public virtual ImageInfo ProfileImage { get; set; }
+ public virtual ImageInfo? ProfileImage { get; set; }
/// <summary>
- /// Gets or sets the user's display preferences.
+ /// Gets the user's display preferences.
/// </summary>
- /// <remarks>
- /// Required.
- /// </remarks>
- [Required]
- public virtual DisplayPreferences DisplayPreferences { get; set; }
+ public virtual ICollection<DisplayPreferences> DisplayPreferences { get; private set; }
/// <summary>
/// Gets or sets the level of sync play permissions this user has.
/// </summary>
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
- /// <summary>
- /// Gets or sets the row version.
- /// </summary>
- /// <remarks>
- /// Required, Concurrency Token.
- /// </remarks>
+ /// <inheritdoc />
[ConcurrencyCheck]
- public uint RowVersion { get; set; }
+ public uint RowVersion { get; private set; }
/// <summary>
- /// Gets or sets the list of access schedules this user has.
+ /// Gets the list of access schedules this user has.
/// </summary>
- public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
+ public virtual ICollection<AccessSchedule> AccessSchedules { get; private set; }
/// <summary>
- /// Gets or sets the list of item display preferences.
+ /// Gets the list of item display preferences.
/// </summary>
- public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; protected set; }
+ public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; private set; }
/*
/// <summary>
- /// Gets or sets the list of groups this user is a member of.
+ /// Gets the list of groups this user is a member of.
/// </summary>
- [ForeignKey("Group_Groups_Guid")]
- public virtual ICollection<Group> Groups { get; protected set; }
+ public virtual ICollection<Group> Groups { get; private set; }
*/
/// <summary>
- /// Gets or sets the list of permissions this user has.
+ /// Gets the list of permissions this user has.
/// </summary>
[ForeignKey("Permission_Permissions_Guid")]
- public virtual ICollection<Permission> Permissions { get; protected set; }
+ public virtual ICollection<Permission> Permissions { get; private set; }
/*
/// <summary>
- /// Gets or sets the list of provider mappings this user has.
+ /// Gets the list of provider mappings this user has.
/// </summary>
- [ForeignKey("ProviderMapping_ProviderMappings_Id")]
- public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
+ public virtual ICollection<ProviderMapping> ProviderMappings { get; private set; }
*/
/// <summary>
- /// Gets or sets the list of preferences this user has.
+ /// Gets the list of preferences this user has.
/// </summary>
[ForeignKey("Preference_Preferences_Guid")]
- public virtual ICollection<Preference> Preferences { get; protected set; }
+ public virtual ICollection<Preference> Preferences { get; private set; }
/// <inheritdoc/>
public void OnSavingChanges()
@@ -494,18 +468,11 @@ namespace Jellyfin.Data.Entities
return Array.IndexOf(GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), id) != -1;
}
- private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
- {
- var localTime = date.ToLocalTime();
- var hour = localTime.TimeOfDay.TotalHours;
-
- return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
- && hour >= schedule.StartHour
- && hour <= schedule.EndHour;
- }
-
+ /// <summary>
+ /// Initializes the default permissions for a user. Should only be called on user creation.
+ /// </summary>
// TODO: make these user configurable?
- private void AddDefaultPermissions()
+ public void AddDefaultPermissions()
{
Permissions.Add(new Permission(PermissionKind.IsAdministrator, false));
Permissions.Add(new Permission(PermissionKind.IsDisabled, false));
@@ -530,12 +497,25 @@ namespace Jellyfin.Data.Entities
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
}
- private void AddDefaultPreferences()
+ /// <summary>
+ /// Initializes the default preferences. Should only be called on user creation.
+ /// </summary>
+ public void AddDefaultPreferences()
{
foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast<PreferenceKind>())
{
Preferences.Add(new Preference(val, string.Empty));
}
}
+
+ private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
+ {
+ var localTime = date.ToLocalTime();
+ var hour = localTime.TimeOfDay.TotalHours;
+
+ return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
+ && hour >= schedule.StartHour
+ && hour <= schedule.EndHour;
+ }
}
}
diff --git a/Jellyfin.Data/Enums/HomeSectionType.cs b/Jellyfin.Data/Enums/HomeSectionType.cs
index e597c9431..9bcd097dc 100644
--- a/Jellyfin.Data/Enums/HomeSectionType.cs
+++ b/Jellyfin.Data/Enums/HomeSectionType.cs
@@ -48,6 +48,11 @@
/// <summary>
/// Live TV.
/// </summary>
- LiveTv = 8
+ LiveTv = 8,
+
+ /// <summary>
+ /// Continue Reading.
+ /// </summary>
+ ResumeBook = 9
}
}
diff --git a/Jellyfin.Data/Interfaces/IHasPermissions.cs b/Jellyfin.Data/Interfaces/IHasPermissions.cs
index 3be72259a..85ee12ad7 100644
--- a/Jellyfin.Data/Interfaces/IHasPermissions.cs
+++ b/Jellyfin.Data/Interfaces/IHasPermissions.cs
@@ -2,7 +2,7 @@
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-namespace Jellyfin.Data
+namespace Jellyfin.Data.Interfaces
{
/// <summary>
/// An abstraction representing an entity that has permissions.
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index a8ac45645..3b14d3312 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -5,6 +5,9 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+ <Nullable>enable</Nullable>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
@@ -24,25 +27,19 @@
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 466a12e67..ee43c2159 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -11,6 +11,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -18,8 +20,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="BlurHashSharp" Version="1.1.1" />
- <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.1" />
+ <PackageReference Include="BlurHashSharp" Version="1.2.0" />
+ <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
<PackageReference Include="SkiaSharp" Version="2.80.2" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" />
</ItemGroup>
@@ -30,16 +32,16 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
+ <ItemGroup>
+ <!-- Needed for https://github.com/dotnet/roslyn-analyzers/issues/4382 which is in the SDK yet -->
+ <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
+ </ItemGroup>
+
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index eab5777d5..8f0fae3be 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -91,9 +91,6 @@ namespace Jellyfin.Drawing.Skia
}
}
- private static bool IsTransparent(SKColor color)
- => (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0;
-
/// <summary>
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
/// </summary>
@@ -111,65 +108,6 @@ namespace Jellyfin.Drawing.Skia
};
}
- private static bool IsTransparentRow(SKBitmap bmp, int row)
- {
- for (var i = 0; i < bmp.Width; ++i)
- {
- if (!IsTransparent(bmp.GetPixel(i, row)))
- {
- return false;
- }
- }
-
- return true;
- }
-
- private static bool IsTransparentColumn(SKBitmap bmp, int col)
- {
- for (var i = 0; i < bmp.Height; ++i)
- {
- if (!IsTransparent(bmp.GetPixel(col, i)))
- {
- return false;
- }
- }
-
- return true;
- }
-
- private SKBitmap CropWhiteSpace(SKBitmap bitmap)
- {
- var topmost = 0;
- while (topmost < bitmap.Height && IsTransparentRow(bitmap, topmost))
- {
- topmost++;
- }
-
- int bottommost = bitmap.Height;
- while (bottommost >= 0 && IsTransparentRow(bitmap, bottommost - 1))
- {
- bottommost--;
- }
-
- var leftmost = 0;
- while (leftmost < bitmap.Width && IsTransparentColumn(bitmap, leftmost))
- {
- leftmost++;
- }
-
- var rightmost = bitmap.Width;
- while (rightmost >= 0 && IsTransparentColumn(bitmap, rightmost - 1))
- {
- rightmost--;
- }
-
- var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
-
- using var image = SKImage.FromBitmap(bitmap);
- using var subset = image.Subset(newRect);
- return SKBitmap.FromImage(subset);
- }
-
/// <inheritdoc />
/// <exception cref="ArgumentNullException">The path is null.</exception>
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
@@ -274,8 +212,8 @@ namespace Jellyfin.Drawing.Skia
if (requiresTransparencyHack || forceCleanBitmap)
{
- using var codec = SKCodec.Create(NormalizePath(path));
- if (codec == null)
+ using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
+ if (res != SKCodecResult.Success)
{
origin = GetSKEncodedOrigin(orientation);
return null;
@@ -312,22 +250,11 @@ namespace Jellyfin.Drawing.Skia
return resultBitmap;
}
- private SKBitmap? GetBitmap(string path, bool cropWhitespace, bool forceAnalyzeBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
- {
- if (cropWhitespace)
- {
- using var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin);
- return bitmap == null ? null : CropWhiteSpace(bitmap);
- }
-
- return Decode(path, forceAnalyzeBitmap, orientation, out origin);
- }
-
- private SKBitmap? GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation)
+ private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
{
if (autoOrient)
{
- var bitmap = GetBitmap(path, cropWhitespace, true, orientation, out var origin);
+ var bitmap = Decode(path, true, orientation, out var origin);
if (bitmap != null && origin != SKEncodedOrigin.TopLeft)
{
@@ -340,16 +267,11 @@ namespace Jellyfin.Drawing.Skia
return bitmap;
}
- return GetBitmap(path, cropWhitespace, false, orientation, out _);
+ return Decode(path, false, orientation, out _);
}
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{
- if (origin == SKEncodedOrigin.Default)
- {
- return bitmap;
- }
-
var needsFlip = origin == SKEncodedOrigin.LeftBottom
|| origin == SKEncodedOrigin.LeftTop
|| origin == SKEncodedOrigin.RightBottom
@@ -447,7 +369,7 @@ namespace Jellyfin.Drawing.Skia
}
/// <inheritdoc/>
- public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
+ public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
{
if (inputPath.Length == 0)
{
@@ -459,14 +381,14 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentException("String can't be empty.", nameof(outputPath));
}
- var skiaOutputFormat = GetImageFormat(selectedOutputFormat);
+ var skiaOutputFormat = GetImageFormat(outputFormat);
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
var blur = options.Blur ?? 0;
var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
- using var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation);
+ using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
if (bitmap == null)
{
throw new InvalidDataException($"Skia unable to read image {inputPath}");
@@ -474,9 +396,7 @@ namespace Jellyfin.Drawing.Skia
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
- if (!options.CropWhiteSpace
- && options.HasDefaultOptions(inputPath, originalImageSize)
- && !autoOrient)
+ if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
{
// Just spit out the original file if all the options are default
return inputPath;
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index e9f9aad57..09a370238 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -68,7 +68,7 @@ namespace Jellyfin.Drawing.Skia
/// <param name="outputPath">The path at which to place the resulting collage image.</param>
/// <param name="width">The desired width of the collage.</param>
/// <param name="height">The desired height of the collage.</param>
- public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
+ public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height)
{
using var bitmap = BuildSquareCollageBitmap(paths, width, height);
using var outputStream = new SKFileWStream(outputPath);
@@ -84,7 +84,7 @@ namespace Jellyfin.Drawing.Skia
/// <param name="width">The desired width of the collage.</param>
/// <param name="height">The desired height of the collage.</param>
/// <param name="libraryName">The name of the library to draw on the collage.</param>
- public void BuildThumbCollage(string[] paths, string outputPath, int width, int height, string? libraryName)
+ public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? libraryName)
{
using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
using var outputStream = new SKFileWStream(outputPath);
@@ -92,7 +92,7 @@ namespace Jellyfin.Drawing.Skia
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
- private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height, string? libraryName)
+ private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName)
{
var bitmap = new SKBitmap(width, height);
@@ -152,14 +152,14 @@ namespace Jellyfin.Drawing.Skia
return bitmap;
}
- private SKBitmap? GetNextValidImage(string[] paths, int currentIndex, out int newIndex)
+ private SKBitmap? GetNextValidImage(IReadOnlyList<string> paths, int currentIndex, out int newIndex)
{
var imagesTested = new Dictionary<int, int>();
SKBitmap? bitmap = null;
- while (imagesTested.Count < paths.Length)
+ while (imagesTested.Count < paths.Count)
{
- if (currentIndex >= paths.Length)
+ if (currentIndex >= paths.Count)
{
currentIndex = 0;
}
@@ -180,7 +180,7 @@ namespace Jellyfin.Drawing.Skia
return bitmap;
}
- private SKBitmap BuildSquareCollageBitmap(string[] paths, int width, int height)
+ private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height)
{
var bitmap = new SKBitmap(width, height);
var imageIndex = 0;
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
index 91bf0015f..faf814c06 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
@@ -1,7 +1,6 @@
#pragma warning disable CA1819 // Properties should not return arrays
using System;
-using MediaBrowser.Model.Configuration;
namespace Jellyfin.Networking.Configuration
{
diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs
index e77b17ba9..8cbe398b0 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs
@@ -1,4 +1,3 @@
-using Jellyfin.Networking.Configuration;
using MediaBrowser.Common.Configuration;
namespace Jellyfin.Networking.Configuration
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index cbda74361..63557e91f 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -5,6 +5,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -13,16 +15,11 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index 8bb97937c..4078fd126 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net;
@@ -164,7 +165,7 @@ namespace Jellyfin.Networking.Manager
{
foreach (var item in source)
{
- result.AddItem(item);
+ result.AddItem(item, false);
}
}
@@ -273,7 +274,7 @@ namespace Jellyfin.Networking.Manager
if (_bindExclusions.Count > 0)
{
// Return all the interfaces except the ones specifically excluded.
- return _interfaceAddresses.Exclude(_bindExclusions);
+ return _interfaceAddresses.Exclude(_bindExclusions, false);
}
if (individualInterfaces)
@@ -284,21 +285,32 @@ namespace Jellyfin.Networking.Manager
// No bind address and no exclusions, so listen on all interfaces.
Collection<IPObject> result = new Collection<IPObject>();
- if (IsIP4Enabled)
+ if (IsIP6Enabled && IsIP4Enabled)
+ {
+ // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any
+ result.AddItem(IPAddress.IPv6Any);
+ }
+ else if (IsIP4Enabled)
{
result.AddItem(IPAddress.Any);
}
-
- if (IsIP6Enabled)
+ else if (IsIP6Enabled)
{
- result.AddItem(IPAddress.IPv6Any);
+ // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses.
+ foreach (var iface in _interfaceAddresses)
+ {
+ if (iface.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ result.AddItem(iface.Address);
+ }
+ }
}
return result;
}
// Remove any excluded bind interfaces.
- return _bindAddresses.Exclude(_bindExclusions);
+ return _bindAddresses.Exclude(_bindExclusions, false);
}
/// <inheritdoc/>
@@ -385,15 +397,26 @@ namespace Jellyfin.Networking.Manager
}
// Get the first LAN interface address that isn't a loopback.
- var interfaces = CreateCollection(_interfaceAddresses
- .Exclude(_bindExclusions)
- .Where(IsInLocalNetwork)
- .OrderBy(p => p.Tag));
+ var interfaces = CreateCollection(
+ _interfaceAddresses
+ .Exclude(_bindExclusions, false)
+ .Where(IsInLocalNetwork)
+ .OrderBy(p => p.Tag));
if (interfaces.Count > 0)
{
if (haveSource)
{
+ foreach (var intf in interfaces)
+ {
+ if (intf.Address.Equals(source.Address))
+ {
+ result = FormatIP6String(intf.Address);
+ _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {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 interfaces)
@@ -413,7 +436,7 @@ namespace Jellyfin.Networking.Manager
}
// There isn't any others, so we'll use the loopback.
- result = IsIP6Enabled ? "::" : "127.0.0.1";
+ result = IsIP6Enabled ? "::1" : "127.0.0.1";
_logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result);
return result;
}
@@ -520,10 +543,10 @@ namespace Jellyfin.Networking.Manager
{
if (filter == null)
{
- return _lanSubnets.Exclude(_excludedSubnets).AsNetworks();
+ return _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks();
}
- return _lanSubnets.Exclude(filter);
+ return _lanSubnets.Exclude(filter, true);
}
/// <inheritdoc/>
@@ -554,7 +577,7 @@ namespace Jellyfin.Networking.Manager
&& ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
|| (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
{
- result.AddItem(iface);
+ result.AddItem(iface, false);
}
}
@@ -564,6 +587,29 @@ namespace Jellyfin.Networking.Manager
return false;
}
+ /// <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.Count > 0 && !IsInLocalNetwork(remoteIp))
+ {
+ // remoteAddressFilter is a whitelist or blacklist.
+ return RemoteAddressFilter.ContainsAddress(remoteIp) == !config.IsRemoteIPFilterBlacklist;
+ }
+ }
+ else if (!IsInLocalNetwork(remoteIp))
+ {
+ // Remote not enabled. So everyone should be LAN.
+ return false;
+ }
+
+ return true;
+ }
+
/// <summary>
/// Reloads all settings and re-initialises the instance.
/// </summary>
@@ -598,8 +644,18 @@ namespace Jellyfin.Networking.Manager
var address = IPNetAddress.Parse(parts[0]);
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
address.Tag = index;
- _interfaceAddresses.AddItem(address);
- _interfaceNames.Add(parts[2], Math.Abs(index));
+ _interfaceAddresses.AddItem(address, false);
+ _interfaceNames[parts[2]] = Math.Abs(index);
+ }
+
+ if (IsIP4Enabled)
+ {
+ _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback);
+ }
+
+ if (IsIP6Enabled)
+ {
+ _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback);
}
}
@@ -691,11 +747,11 @@ namespace Jellyfin.Networking.Manager
/// Checks the string to see if it matches any interface names.
/// </summary>
/// <param name="token">String to check.</param>
- /// <param name="index">Interface index number.</param>
+ /// <param name="index">Interface index numbers that match.</param>
/// <returns><c>true</c> if an interface name matches the token, <c>False</c> otherwise.</returns>
- private bool IsInterface(string token, out int index)
+ private bool TryGetInterfaces(string token, [NotNullWhen(true)] out List<int>? index)
{
- index = -1;
+ index = null;
// Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
// Null check required here for automated testing.
@@ -712,13 +768,13 @@ namespace Jellyfin.Networking.Manager
if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase))
|| (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture)))
{
- index = interfcIndex;
- return true;
+ index ??= new List<int>();
+ index.Add(interfcIndex);
}
}
}
- return false;
+ return index != null;
}
/// <summary>
@@ -730,14 +786,14 @@ namespace Jellyfin.Networking.Manager
{
// Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
// Null check required here for automated testing.
- if (IsInterface(token, out int index))
+ if (TryGetInterfaces(token, out var indices))
{
_logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token);
- // Replace interface tags with the interface IP's.
+ // Replace all the interface tags with the interface IP's.
foreach (IPNetAddress iface in _interfaceAddresses)
{
- if (Math.Abs(iface.Tag) == index
+ if (indices.Contains(Math.Abs(iface.Tag))
&& ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
|| (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
{
@@ -916,16 +972,24 @@ namespace Jellyfin.Networking.Manager
// Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
if (config.IgnoreVirtualInterfaces)
{
- var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',');
- var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length];
- Array.Copy(lanAddresses, newList, lanAddresses.Length);
- Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length);
- lanAddresses = newList;
+ // each virtual interface name must be pre-pended with the exclusion symbol !
+ var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray();
+ if (lanAddresses.Length > 0)
+ {
+ var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length];
+ Array.Copy(lanAddresses, newList, lanAddresses.Length);
+ Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length);
+ lanAddresses = newList;
+ }
+ else
+ {
+ lanAddresses = virtualInterfaceNames;
+ }
}
// Read and parse bind addresses and exclusions, removing ones that don't exist.
- _bindAddresses = CreateIPCollection(lanAddresses).Union(_interfaceAddresses);
- _bindExclusions = CreateIPCollection(lanAddresses, true).Union(_interfaceAddresses);
+ _bindAddresses = CreateIPCollection(lanAddresses).ThatAreContainedInNetworks(_interfaceAddresses);
+ _bindExclusions = CreateIPCollection(lanAddresses, true).ThatAreContainedInNetworks(_interfaceAddresses);
_logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString());
_logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString());
}
@@ -1003,12 +1067,12 @@ namespace Jellyfin.Networking.Manager
}
// Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet.
- _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i)));
+ _internalInterfaces = CreateCollection(_interfaceAddresses.Where(IsInLocalNetwork));
}
_logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString());
_logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString());
- _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks().AsString());
+ _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString());
}
}
@@ -1062,7 +1126,7 @@ namespace Jellyfin.Networking.Manager
nw.Tag *= -1;
}
- _interfaceAddresses.AddItem(nw);
+ _interfaceAddresses.AddItem(nw, false);
// Store interface name so we can use the name in Collections.
_interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
@@ -1083,7 +1147,7 @@ namespace Jellyfin.Networking.Manager
nw.Tag *= -1;
}
- _interfaceAddresses.AddItem(nw);
+ _interfaceAddresses.AddItem(nw, false);
// Store interface name so we can use the name in Collections.
_interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
@@ -1099,36 +1163,40 @@ namespace Jellyfin.Networking.Manager
}
#pragma warning restore CA1031 // Do not catch general exception types
}
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error in InitialiseInterfaces.");
+ }
- _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count);
- _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString());
+ // If for some reason we don't have an interface info, resolve our DNS name.
+ if (_interfaceAddresses.Count == 0)
+ {
+ _logger.LogError("No interfaces information available. Resolving DNS name.");
+ IPHost host = new IPHost(Dns.GetHostName());
+ foreach (var a in host.GetAddresses())
+ {
+ _interfaceAddresses.AddItem(a);
+ }
- // If for some reason we don't have an interface info, resolve our DNS name.
if (_interfaceAddresses.Count == 0)
{
- _logger.LogError("No interfaces information available. Resolving DNS name.");
- IPHost host = new IPHost(Dns.GetHostName());
- foreach (var a in host.GetAddresses())
- {
- _interfaceAddresses.AddItem(a);
- }
-
- if (_interfaceAddresses.Count == 0)
- {
- _logger.LogWarning("No interfaces information available. Using loopback.");
- // Last ditch attempt - use loopback address.
- _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback);
- if (IsIP6Enabled)
- {
- _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback);
- }
- }
+ _logger.LogWarning("No interfaces information available. Using loopback.");
}
}
- catch (NetworkInformationException ex)
+
+ if (IsIP4Enabled)
{
- _logger.LogError(ex, "Error in InitialiseInterfaces.");
+ _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback);
+ }
+
+ if (IsIP6Enabled)
+ {
+ _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback);
}
+
+ _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count);
+ _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString());
}
}
@@ -1197,7 +1265,7 @@ namespace Jellyfin.Networking.Manager
private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result)
{
result = string.Empty;
- var addresses = _bindAddresses.Exclude(_bindExclusions);
+ var addresses = _bindAddresses.Exclude(_bindExclusions, false);
int count = addresses.Count;
if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any)))
@@ -1282,7 +1350,7 @@ namespace Jellyfin.Networking.Manager
result = string.Empty;
// Get the first WAN interface address that isn't a loopback.
var extResult = _interfaceAddresses
- .Exclude(_bindExclusions)
+ .Exclude(_bindExclusions, false)
.Where(p => !IsInLocalNetwork(p))
.OrderBy(p => p.Tag);
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
index 2f9f44ed6..8b0bd84c6 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
@@ -29,20 +29,20 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
}
/// <inheritdoc />
- public async Task OnEvent(GenericEventArgs<AuthenticationResult> e)
+ public async Task OnEvent(GenericEventArgs<AuthenticationResult> eventArgs)
{
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
- e.Argument.User.Name),
+ eventArgs.Argument.User.Name),
"AuthenticationSucceeded",
- e.Argument.User.Id)
+ eventArgs.Argument.User.Id)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localizationManager.GetLocalizedString("LabelIpAddressValue"),
- e.Argument.SessionInfo.RemoteEndPoint),
+ eventArgs.Argument.SessionInfo.RemoteEndPoint),
}).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
index 0340248bb..aa6015caa 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
@@ -86,7 +86,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string? GetPlaybackNotificationType(string mediaType)
+ private static string GetPlaybackNotificationType(string mediaType)
{
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
@@ -98,7 +98,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return NotificationType.VideoPlayback.ToString();
}
- return null;
+ return "Playback";
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
index 05201a346..cbc9f3017 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
@@ -33,10 +33,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
}
/// <inheritdoc />
- public async Task OnEvent(TaskCompletionEventArgs e)
+ public async Task OnEvent(TaskCompletionEventArgs eventArgs)
{
- var result = e.Result;
- var task = e.Task;
+ var result = eventArgs.Result;
+ var task = eventArgs.Task;
if (task.ScheduledTask is IConfigurableScheduledTask activityTask
&& !activityTask.IsLogged)
@@ -54,14 +54,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
{
var vals = new List<string>();
- if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
+ if (!string.IsNullOrEmpty(eventArgs.Result.ErrorMessage))
{
- vals.Add(e.Result.ErrorMessage);
+ vals.Add(eventArgs.Result.ErrorMessage);
}
- if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
+ if (!string.IsNullOrEmpty(eventArgs.Result.LongErrorMessage))
{
- vals.Add(e.Result.LongErrorMessage);
+ vals.Add(eventArgs.Result.LongErrorMessage);
}
await _activityManager.CreateAsync(new ActivityLog(
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
index 91a30069e..eb7572ac6 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
@@ -30,13 +30,13 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
}
/// <inheritdoc />
- public async Task OnEvent(PluginUninstalledEventArgs e)
+ public async Task OnEvent(PluginUninstalledEventArgs eventArgs)
{
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localizationManager.GetLocalizedString("PluginUninstalledWithName"),
- e.Argument.Name),
+ eventArgs.Argument.Name),
NotificationType.PluginUninstalled.ToString(),
Guid.Empty))
.ConfigureAwait(false);
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
index a14911b94..9beb6f2f2 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
@@ -30,12 +30,12 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
}
/// <inheritdoc />
- public async Task OnEvent(UserUpdatedEventArgs e)
+ public async Task OnEvent(UserUpdatedEventArgs eventArgs)
{
await _sessionManager.SendMessageToUserSessions(
- new List<Guid> { e.Argument.Id },
+ new List<Guid> { eventArgs.Argument.Id },
SessionMessageType.UserUpdated,
- _userManager.GetUserDto(e.Argument),
+ _userManager.GetUserDto(eventArgs.Argument),
CancellationToken.None).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Server.Implementations/Events/EventManager.cs b/Jellyfin.Server.Implementations/Events/EventManager.cs
index 707002442..8c5d8f2ce 100644
--- a/Jellyfin.Server.Implementations/Events/EventManager.cs
+++ b/Jellyfin.Server.Implementations/Events/EventManager.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Server.Implementations.Events
public void Publish<T>(T eventArgs)
where T : EventArgs
{
- Task.WaitAll(PublishInternal(eventArgs));
+ PublishInternal(eventArgs).GetAwaiter().GetResult();
}
/// <inheritdoc />
@@ -43,7 +43,12 @@ namespace Jellyfin.Server.Implementations.Events
private async Task PublishInternal<T>(T eventArgs)
where T : EventArgs
{
- using var scope = _appHost.ServiceProvider.CreateScope();
+ using var scope = _appHost.ServiceProvider?.CreateScope();
+ if (scope == null)
+ {
+ return;
+ }
+
foreach (var service in scope.ServiceProvider.GetServices<IEventConsumer<T>>())
{
try
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 4f24da0ee..eeeb1d19b 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -6,6 +6,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -14,7 +16,6 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -26,11 +27,13 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.7" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 39f842354..db648472d 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -149,21 +149,77 @@ namespace Jellyfin.Server.Implementations
modelBuilder.HasDefaultSchema("jellyfin");
+ // Collations
+
+ modelBuilder.Entity<User>()
+ .Property(user => user.Username)
+ .UseCollation("NOCASE");
+
+ // Delete behavior
+
+ modelBuilder.Entity<User>()
+ .HasOne(u => u.ProfileImage)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity<User>()
+ .HasMany(u => u.Permissions)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity<User>()
+ .HasMany(u => u.Preferences)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity<User>()
+ .HasMany(u => u.AccessSchedules)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity<User>()
+ .HasMany(u => u.DisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity<User>()
+ .HasMany(u => u.ItemDisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
modelBuilder.Entity<DisplayPreferences>()
- .HasIndex(entity => entity.UserId)
- .IsUnique(false);
+ .HasMany(d => d.HomeSections)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ // Indexes
+
+ modelBuilder.Entity<User>()
+ .HasIndex(entity => entity.Username)
+ .IsUnique();
modelBuilder.Entity<DisplayPreferences>()
.HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
.IsUnique();
modelBuilder.Entity<CustomItemDisplayPreferences>()
- .HasIndex(entity => entity.UserId)
- .IsUnique(false);
-
- modelBuilder.Entity<CustomItemDisplayPreferences>()
.HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
.IsUnique();
+
+ // Used to get a user's permissions or a specific permission for a user.
+ // Also prevents multiple values being created for a user.
+ // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
+ modelBuilder.Entity<Permission>()
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+
+ modelBuilder.Entity<Preference>()
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
}
}
}
diff --git a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs
new file mode 100644
index 000000000..869676824
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs
@@ -0,0 +1,535 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDb))]
+ [Migration("20210320181425_AddIndexesAndCollations")]
+ partial class AddIndexesAndCollations
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "5.0.3");
+
+ 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.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")
+ .IsRequired()
+ .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.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<string>("EasyPassword")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ 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.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/20210320181425_AddIndexesAndCollations.cs b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.cs
new file mode 100644
index 000000000..506e4ae66
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.cs
@@ -0,0 +1,240 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class AddIndexesAndCollations : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_ImageInfos_Users_UserId",
+ schema: "jellyfin",
+ table: "ImageInfos");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Permissions_Users_Permission_Permissions_Guid",
+ schema: "jellyfin",
+ table: "Permissions");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Preferences_Users_Preference_Preferences_Guid",
+ schema: "jellyfin",
+ table: "Preferences");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Preferences_Preference_Preferences_Guid",
+ schema: "jellyfin",
+ table: "Preferences");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Permissions_Permission_Permissions_Guid",
+ schema: "jellyfin",
+ table: "Permissions");
+
+ migrationBuilder.DropIndex(
+ name: "IX_DisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "DisplayPreferences");
+
+ migrationBuilder.DropIndex(
+ name: "IX_CustomItemDisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "CustomItemDisplayPreferences");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Username",
+ schema: "jellyfin",
+ table: "Users",
+ type: "TEXT",
+ maxLength: 255,
+ nullable: false,
+ collation: "NOCASE",
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldMaxLength: 255);
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "UserId",
+ schema: "jellyfin",
+ table: "Preferences",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "UserId",
+ schema: "jellyfin",
+ table: "Permissions",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.Sql("UPDATE Preferences SET UserId = Preference_Preferences_Guid");
+ migrationBuilder.Sql("UPDATE Permissions SET UserId = Permission_Permissions_Guid");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_Username",
+ schema: "jellyfin",
+ table: "Users",
+ column: "Username",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Preferences_UserId_Kind",
+ schema: "jellyfin",
+ table: "Preferences",
+ columns: new[] { "UserId", "Kind" },
+ unique: true,
+ filter: "[UserId] IS NOT NULL");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Permissions_UserId_Kind",
+ schema: "jellyfin",
+ table: "Permissions",
+ columns: new[] { "UserId", "Kind" },
+ unique: true,
+ filter: "[UserId] IS NOT NULL");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_ImageInfos_Users_UserId",
+ schema: "jellyfin",
+ table: "ImageInfos",
+ column: "UserId",
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Permissions_Users_UserId",
+ schema: "jellyfin",
+ table: "Permissions",
+ column: "UserId",
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Preferences_Users_UserId",
+ schema: "jellyfin",
+ table: "Preferences",
+ column: "UserId",
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_ImageInfos_Users_UserId",
+ schema: "jellyfin",
+ table: "ImageInfos");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Permissions_Users_UserId",
+ schema: "jellyfin",
+ table: "Permissions");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_Preferences_Users_UserId",
+ schema: "jellyfin",
+ table: "Preferences");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Users_Username",
+ schema: "jellyfin",
+ table: "Users");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Preferences_UserId_Kind",
+ schema: "jellyfin",
+ table: "Preferences");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Permissions_UserId_Kind",
+ schema: "jellyfin",
+ table: "Permissions");
+
+ migrationBuilder.DropColumn(
+ name: "UserId",
+ schema: "jellyfin",
+ table: "Preferences");
+
+ migrationBuilder.DropColumn(
+ name: "UserId",
+ schema: "jellyfin",
+ table: "Permissions");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Username",
+ schema: "jellyfin",
+ table: "Users",
+ type: "TEXT",
+ maxLength: 255,
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldMaxLength: 255,
+ oldCollation: "NOCASE");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Preferences_Preference_Preferences_Guid",
+ schema: "jellyfin",
+ table: "Preferences",
+ column: "Preference_Preferences_Guid");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Permissions_Permission_Permissions_Guid",
+ schema: "jellyfin",
+ table: "Permissions",
+ column: "Permission_Permissions_Guid");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "DisplayPreferences",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_CustomItemDisplayPreferences_UserId",
+ schema: "jellyfin",
+ table: "CustomItemDisplayPreferences",
+ column: "UserId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_ImageInfos_Users_UserId",
+ schema: "jellyfin",
+ table: "ImageInfos",
+ column: "UserId",
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Permissions_Users_Permission_Permissions_Guid",
+ schema: "jellyfin",
+ table: "Permissions",
+ column: "Permission_Permissions_Guid",
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Preferences_Users_Preference_Preferences_Guid",
+ schema: "jellyfin",
+ table: "Preferences",
+ column: "Preference_Preferences_Guid",
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
new file mode 100644
index 000000000..d332d19f2
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
@@ -0,0 +1,520 @@
+#pragma warning disable CS1591
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDb))]
+ [Migration("20210407110544_NullableCustomPrefValue")]
+ partial class NullableCustomPrefValue
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "5.0.3");
+
+ 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.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");
+
+ 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");
+
+ 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<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Permission_Permissions_Guid");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Preference_Preferences_Guid");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .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<string>("EasyPassword")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ 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");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .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");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("Permission_Permissions_Guid");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("Preference_Preferences_Guid");
+ });
+
+ 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/20210407110544_NullableCustomPrefValue.cs b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.cs
new file mode 100644
index 000000000..ade68612c
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.cs
@@ -0,0 +1,35 @@
+#pragma warning disable CS1591
+// <auto-generated />
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class NullableCustomPrefValue : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "Value",
+ schema: "jellyfin",
+ table: "CustomItemDisplayPreferences",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "Value",
+ schema: "jellyfin",
+ table: "CustomItemDisplayPreferences",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: "",
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 1614a88ef..286eb7468 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
- .HasAnnotation("ProductVersion", "5.0.0");
+ .HasAnnotation("ProductVersion", "5.0.3");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -110,13 +110,10 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("TEXT");
b.Property<string>("Value")
- .IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
- b.HasIndex("UserId");
-
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
@@ -174,8 +171,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
- b.HasIndex("UserId");
-
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
@@ -289,12 +284,17 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsConcurrencyToken()
.HasColumnType("INTEGER");
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
- b.HasIndex("Permission_Permissions_Guid");
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
});
@@ -315,6 +315,9 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsConcurrencyToken()
.HasColumnType("INTEGER");
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
@@ -322,7 +325,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
- b.HasIndex("Preference_Preferences_Guid");
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
});
@@ -429,10 +434,14 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
- .HasColumnType("TEXT");
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
b.HasKey("Id");
+ b.HasIndex("Username")
+ .IsUnique();
+
b.ToTable("Users");
});
@@ -448,8 +457,8 @@ namespace Jellyfin.Server.Implementations.Migrations
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
- .WithOne("DisplayPreferences")
- .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@@ -467,7 +476,8 @@ namespace Jellyfin.Server.Implementations.Migrations
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
- .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
@@ -483,14 +493,16 @@ namespace Jellyfin.Server.Implementations.Migrations
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
- .HasForeignKey("Permission_Permissions_Guid");
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
- .HasForeignKey("Preference_Preferences_Guid");
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
@@ -502,8 +514,7 @@ namespace Jellyfin.Server.Implementations.Migrations
{
b.Navigation("AccessSchedules");
- b.Navigation("DisplayPreferences")
- .IsRequired();
+ b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");
diff --git a/Jellyfin.Server.Implementations/Properties/AssemblyInfo.cs b/Jellyfin.Server.Implementations/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..6528d4fdc
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Properties/AssemblyInfo.cs
@@ -0,0 +1,23 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+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("Jellyfin.Server.Implementations")]
+[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.Server.Implementations.Tests")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index 9cc1c3e5e..c99c5e4ef 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -66,7 +66,7 @@ namespace Jellyfin.Server.Implementations.Users
else if (string.Equals(
spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
pin.Replace("-", string.Empty, StringComparison.Ordinal),
- StringComparison.InvariantCultureIgnoreCase))
+ StringComparison.OrdinalIgnoreCase))
{
var resetUser = userManager.GetUserByName(spr.UserName)
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index c8a589cab..c89e3c74d 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -1,4 +1,5 @@
#pragma warning disable CA1307
+#pragma warning disable CA1309
using System;
using System.Collections.Generic;
@@ -35,7 +36,7 @@ namespace Jellyfin.Server.Implementations.Users
if (prefs == null)
{
- prefs = new DisplayPreferences(userId, itemId, client);
+ prefs = new DisplayPreferences(userId, itemId, client);
_dbContext.DisplayPreferences.Add(prefs);
}
@@ -67,7 +68,7 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc />
- public IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
+ public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
return _dbContext.CustomItemDisplayPreferences
.AsQueryable()
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index d1de5408c..27d4f40d3 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -137,17 +137,20 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentNullException(nameof(user));
}
- if (string.IsNullOrWhiteSpace(newName))
- {
- throw new ArgumentException("Invalid username", nameof(newName));
- }
+ ThrowIfInvalidUsername(newName);
if (user.Username.Equals(newName, StringComparison.Ordinal))
{
throw new ArgumentException("The new and old names must be different.");
}
- if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal)))
+ await using var dbContext = _dbProvider.CreateContext();
+
+ if (await dbContext.Users
+ .AsQueryable()
+ .Where(u => u.Username == newName && u.Id != user.Id)
+ .AnyAsync()
+ .ConfigureAwait(false))
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
@@ -187,12 +190,15 @@ namespace Jellyfin.Server.Implementations.Users
var user = new User(
name,
- _defaultAuthenticationProvider.GetType().FullName,
- _defaultPasswordResetProvider.GetType().FullName)
+ _defaultAuthenticationProvider.GetType().FullName!,
+ _defaultPasswordResetProvider.GetType().FullName!)
{
InternalId = max + 1
};
+ user.AddDefaultPermissions();
+ user.AddDefaultPreferences();
+
_users.Add(user.Id, user);
return user;
@@ -201,9 +207,14 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task<User> CreateUserAsync(string name)
{
- if (!IsValidUsername(name))
+ ThrowIfInvalidUsername(name);
+
+ if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
- throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+ throw new ArgumentException(string.Format(
+ CultureInfo.InvariantCulture,
+ "A user with the name '{0}' already exists.",
+ name));
}
await using var dbContext = _dbProvider.CreateContext();
@@ -246,16 +257,6 @@ namespace Jellyfin.Server.Implementations.Users
}
await using var dbContext = _dbProvider.CreateContext();
-
- // Clear all entities related to the user from the database.
- if (user.ProfileImage != null)
- {
- dbContext.Remove(user.ProfileImage);
- }
-
- dbContext.RemoveRange(user.Permissions);
- dbContext.RemoveRange(user.Preferences);
- dbContext.RemoveRange(user.AccessSchedules);
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
_users.Remove(userId);
@@ -402,27 +403,18 @@ namespace Jellyfin.Server.Implementations.Users
}
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
- bool success;
- IAuthenticationProvider? authenticationProvider;
+ var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
+ .ConfigureAwait(false);
+ var authenticationProvider = authResult.authenticationProvider;
+ var success = authResult.success;
- if (user != null)
- {
- var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
- .ConfigureAwait(false);
- authenticationProvider = authResult.authenticationProvider;
- success = authResult.success;
- }
- else
+ if (user == null)
{
- var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint)
- .ConfigureAwait(false);
- authenticationProvider = authResult.authenticationProvider;
string updatedUsername = authResult.username;
- success = authResult.success;
if (success
&& authenticationProvider != null
- && !(authenticationProvider is DefaultAuthenticationProvider))
+ && authenticationProvider is not DefaultAuthenticationProvider)
{
// Trust the username returned by the authentication provider
username = updatedUsername;
@@ -442,7 +434,7 @@ namespace Jellyfin.Server.Implementations.Users
{
var providerId = authenticationProvider.GetType().FullName;
- if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
+ if (providerId != null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
{
user.AuthenticationProviderId = providerId;
await UpdateUserAsync(user).ConfigureAwait(false);
@@ -725,12 +717,22 @@ namespace Jellyfin.Server.Implementations.Users
_users[user.Id] = user;
}
+ internal static void ThrowIfInvalidUsername(string name)
+ {
+ if (!string.IsNullOrWhiteSpace(name) && IsValidUsername(name))
+ {
+ return;
+ }
+
+ throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name));
+ }
+
private static bool IsValidUsername(string name)
{
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
- return Regex.IsMatch(name, @"^[\w\ \-'._@]*$");
+ return Regex.IsMatch(name, @"^[\w\ \-'._@]+$");
}
private IAuthenticationProvider GetAuthenticationProvider(User user)
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index b76aa5e14..94c3ca4a9 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -11,7 +11,6 @@ using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Users;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Drawing;
@@ -21,6 +20,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -37,18 +37,21 @@ namespace Jellyfin.Server
/// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
+ /// <param name="startupConfig">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public CoreAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
+ IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection collection)
: base(
applicationPaths,
loggerFactory,
options,
+ startupConfig,
fileSystem,
collection)
{
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 88e2b4152..e29167747 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -79,6 +79,16 @@ namespace Jellyfin.Server.Extensions
}
/// <summary>
+ /// Enables url decoding before binding to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseQueryStringDecoding(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<QueryStringDecodingMiddleware>();
+ }
+
+ /// <summary>
/// Adds base url redirection to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 545937207..924b250ce 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -225,14 +225,13 @@ namespace Jellyfin.Server.Extensions
.AddJsonOptions(options =>
{
// Update all properties that are set in JsonDefaults
- var jsonOptions = JsonDefaults.GetPascalCaseOptions();
+ var jsonOptions = JsonDefaults.PascalCaseOptions;
// From JsonDefaults
options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
- options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive;
options.JsonSerializerOptions.Converters.Clear();
foreach (var converter in jsonOptions.Converters)
@@ -261,15 +260,16 @@ namespace Jellyfin.Server.Extensions
{
return serviceCollection.AddSwaggerGen(c =>
{
+ var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1";
c.SwaggerDoc("api-docs", new OpenApiInfo
{
Title = "Jellyfin API",
- Version = "v1",
+ Version = version,
Extensions = new Dictionary<string, IOpenApiExtension>
{
{
"x-jellyfin-version",
- new OpenApiString(typeof(ApplicationHost).Assembly.GetName().Version?.ToString())
+ new OpenApiString(version)
}
}
});
@@ -310,6 +310,7 @@ namespace Jellyfin.Server.Extensions
// Allow parameters to properly be nullable.
c.UseAllOfToExtendReferenceSchemas();
+ c.SupportNonNullableReferenceTypes();
// TODO - remove when all types are supported in System.Text.Json
c.AddSwaggerTypeMappings();
@@ -318,7 +319,7 @@ namespace Jellyfin.Server.Extensions
c.OperationFilter<FileResponseFilter>();
c.OperationFilter<FileRequestFilter>();
c.OperationFilter<ParameterObsoleteFilter>();
- c.DocumentFilter<WebsocketModelFilter>();
+ c.DocumentFilter<AdditionalModelFilter>();
});
}
diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs
index 248802857..87a59e0b4 100644
--- a/Jellyfin.Server/Filters/WebsocketModelFilter.cs
+++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs
@@ -1,5 +1,6 @@
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.ApiClient;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
@@ -9,9 +10,9 @@ using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
{
/// <summary>
- /// Add models used in websocket messaging.
+ /// Add models not directly used by the API, but used for discovery and websockets.
/// </summary>
- public class WebsocketModelFilter : IDocumentFilter
+ public class AdditionalModelFilter : IDocumentFilter
{
/// <inheritdoc />
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
@@ -25,6 +26,9 @@ namespace Jellyfin.Server.Filters
context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
+
+ context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
}
}
}
diff --git a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs
index e54044d0e..b9ce221f5 100644
--- a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs
+++ b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs
@@ -1,7 +1,6 @@
using System;
using System.Linq;
using Jellyfin.Api.Attributes;
-using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
index 8043989b1..c349e3dca 100644
--- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
/// <summary>
/// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
/// </summary>
- public CamelCaseJsonProfileFormatter() : base(JsonDefaults.GetCamelCaseOptions())
+ public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions)
{
SupportedMediaTypes.Clear();
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));
diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
index e8dd48e4e..cfc9d1ad3 100644
--- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs
+++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Text;
+using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
index d0110b125..0480f5e0e 100644
--- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -13,7 +13,7 @@ namespace Jellyfin.Server.Formatters
/// <summary>
/// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
/// </summary>
- public PascalCaseJsonProfileFormatter() : base(JsonDefaults.GetPascalCaseOptions())
+ public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions)
{
SupportedMediaTypes.Clear();
// Add application/json for default formatter
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index bf4f80669..ea782cb66 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -9,11 +9,14 @@
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
+ <ServerGarbageCollection>false</ServerGarbageCollection>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
- <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+ <!-- <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers> -->
</PropertyGroup>
<ItemGroup>
@@ -26,25 +29,20 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.3" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.3" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.7" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.7" />
<PackageReference Include="prometheus-net" Version="4.1.1" />
<PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" />
- <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
+ <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index c23da2fd6..2eef223e5 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -45,15 +45,33 @@ namespace Jellyfin.Server.Middleware
var localPath = httpContext.Request.Path.ToString();
var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
- if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
- || string.IsNullOrEmpty(localPath)
- || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(baseUrlPrefix))
{
- // Always redirect back to the default path if the base prefix is invalid or missing
+ var startsWithBaseUrl = localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase);
+
+ if (!startsWithBaseUrl
+ && (localPath.Equals("/health", StringComparison.OrdinalIgnoreCase)
+ || localPath.Equals("/health/", StringComparison.OrdinalIgnoreCase)))
+ {
+ _logger.LogDebug("Redirecting /health check");
+ httpContext.Response.Redirect(baseUrlPrefix + "/health");
+ return;
+ }
+
+ if (!startsWithBaseUrl)
+ {
+ // Always redirect back to the default path if the base prefix is invalid or missing
+ _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
+ httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ return;
+ }
+ }
+ else if (string.IsNullOrEmpty(localPath)
+ || localPath.Equals("/", StringComparison.Ordinal))
+ {
+ // Always redirect back to the default path if root is requested.
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
- httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ httpContext.Response.Redirect("/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
return;
}
diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
index 525cd9ffe..0afcd61a0 100644
--- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
+++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
@@ -1,9 +1,7 @@
using System.Net;
using System.Threading.Tasks;
-using Jellyfin.Networking.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Server.Middleware
@@ -29,9 +27,8 @@ namespace Jellyfin.Server.Middleware
/// </summary>
/// <param name="httpContext">The current HTTP context.</param>
/// <param name="networkManager">The network manager.</param>
- /// <param name="serverConfigurationManager">The server configuration manager.</param>
/// <returns>The async task.</returns>
- public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
+ public async Task Invoke(HttpContext httpContext, INetworkManager networkManager)
{
if (httpContext.IsLocal())
{
@@ -42,32 +39,8 @@ namespace Jellyfin.Server.Middleware
var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
- if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
+ if (!networkManager.HasRemoteAccess(remoteIp))
{
- // 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.
- var remoteAddressFilter = networkManager.RemoteAddressFilter;
-
- if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp))
- {
- // remoteAddressFilter is a whitelist or blacklist.
- bool isListed = remoteAddressFilter.ContainsAddress(remoteIp);
- if (!serverConfigurationManager.GetNetworkConfiguration().IsRemoteIPFilterBlacklist)
- {
- // Black list, so flip over.
- isListed = !isListed;
- }
-
- if (!isListed)
- {
- // If your name isn't on the list, you arn't coming in.
- return;
- }
- }
- }
- else if (!networkManager.IsInLocalNetwork(remoteIp))
- {
- // Remote not enabled. So everyone should be LAN.
return;
}
diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
index 8065054a1..67bf24d2a 100644
--- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
+++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
@@ -1,9 +1,6 @@
-using System;
-using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Jellyfin.Networking.Configuration;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
new file mode 100644
index 000000000..fd0ebbf43
--- /dev/null
+++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// URL decodes the querystring before binding.
+ /// </summary>
+ public class QueryStringDecodingMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public QueryStringDecodingMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext)
+ {
+ httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>()));
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
new file mode 100644
index 000000000..310a3d31a
--- /dev/null
+++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using MediaBrowser.Common.Extensions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Defines the <see cref="UrlDecodeQueryFeature"/>.
+ /// </summary>
+ public class UrlDecodeQueryFeature : IQueryFeature
+ {
+ private IQueryCollection? _store;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class.
+ /// </summary>
+ /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param>
+ public UrlDecodeQueryFeature(IQueryFeature feature)
+ {
+ Query = feature.Query;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>.
+ /// </summary>
+ public IQueryCollection Query
+ {
+ get
+ {
+ return _store ?? QueryCollection.Empty;
+ }
+
+ set
+ {
+ // Only interested in where the querystring is encoded which shows up as one key with nothing in the value.
+ if (value.Count != 1)
+ {
+ _store = value;
+ return;
+ }
+
+ // Encoded querystrings have no value, so don't process anything if a value is present.
+ var (key, stringValues) = value.First();
+ if (!string.IsNullOrEmpty(stringValues))
+ {
+ _store = value;
+ return;
+ }
+
+ // Unencode and re-parse querystring.
+ var unencodedKey = HttpUtility.UrlDecode(key);
+
+ if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
+ {
+ // Don't do anything if it's not encoded.
+ _store = value;
+ return;
+ }
+
+ var pairs = new Dictionary<string, StringValues>();
+ var queryString = unencodedKey.SpanSplit('&');
+
+ foreach (var pair in queryString)
+ {
+ var i = pair.IndexOf('=');
+ if (i == -1)
+ {
+ // encoded is an equals.
+ // We use TryAdd so duplicate keys get ignored
+ pairs.TryAdd(pair.ToString(), StringValues.Empty);
+ continue;
+ }
+
+ var k = pair[..i].ToString();
+ var v = pair[(i + 1)..].ToString();
+ if (!pairs.TryAdd(k, new StringValues(v)))
+ {
+ pairs[k] = StringValues.Concat(pairs[k], v);
+ }
+ }
+
+ _store = new QueryCollection(pairs);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs
index 816dd9ee7..c9710f1fd 100644
--- a/Jellyfin.Server/Migrations/MigrationOptions.cs
+++ b/Jellyfin.Server/Migrations/MigrationOptions.cs
@@ -16,9 +16,12 @@ namespace Jellyfin.Server.Migrations
Applied = new List<(Guid Id, string Name)>();
}
+// .Net xml serializer can't handle interfaces
+#pragma warning disable CA1002 // Do not expose generic lists
/// <summary>
/// Gets the list of applied migration routine names.
/// </summary>
public List<(Guid Id, string Name)> Applied { get; }
+#pragma warning restore CA1002
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 305660ae6..cf938ab8c 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -40,15 +40,15 @@ namespace Jellyfin.Server.Migrations
.Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
.OfType<IMigrationRoutine>()
.ToArray();
- var migrationOptions = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
+ var migrationOptions = ((IConfigurationManager)host.ConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
- if (!host.ServerConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
+ if (!host.ConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
{
// If startup wizard is not finished, this is a fresh install.
// Don't run any migrations, just mark all of them as applied.
logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name)));
- host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+ host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
}
var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
@@ -77,7 +77,7 @@ namespace Jellyfin.Server.Migrations
// Mark the migration as completed
logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name);
migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
- host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+ host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs b/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs
index 2521d9952..6343c422d 100644
--- a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs
+++ b/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs
@@ -41,9 +41,9 @@ namespace Jellyfin.Server.Migrations.Routines
var databasePath = Path.Join(_serverApplicationPaths.DataPath, DbFilename);
using var connection = SQLite3.Open(databasePath, ConnectionFlags.ReadWrite, null);
_logger.LogInformation("Creating index idx_TypedBaseItemsUserDataKeyType");
- connection.Execute("CREATE INDEX idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);");
+ connection.Execute("CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);");
_logger.LogInformation("Creating index idx_PeopleNameListOrder");
- connection.Execute("CREATE INDEX idx_PeopleNameListOrder ON People(Name, ListOrder);");
+ connection.Execute("CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder);");
}
}
-} \ No newline at end of file
+}
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
index 6821630db..ee4f8b0ba 100644
--- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
+++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
@@ -75,7 +75,7 @@ namespace Jellyfin.Server.Migrations.Routines
{
var existingConfigJson = JToken.Parse(File.ReadAllText(oldConfigPath));
return _defaultConfigHistory
- .Select(historicalConfigText => JToken.Parse(historicalConfigText))
+ .Select(JToken.Parse)
.Any(historicalConfigJson => JToken.DeepEquals(existingConfigJson, historicalConfigJson));
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
index 0925a87b5..378e88e25 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -1,6 +1,5 @@
using System;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines
@@ -32,7 +31,7 @@ namespace Jellyfin.Server.Migrations.Routines
public void Perform()
{
// Set EnableThrottling to false since it wasn't used before and may introduce issues
- var encoding = _configManager.GetConfiguration<EncodingOptions>("encoding");
+ var encoding = _configManager.GetEncodingOptions();
if (encoding.EnableThrottling)
{
_logger.LogInformation("Disabling transcoding throttling during migration");
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 07829c696..e25d29122 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -126,13 +126,13 @@ namespace Jellyfin.Server.Migrations.Routines
ShowSidebar = dto.ShowSidebar,
ScrollDirection = dto.ScrollDirection,
ChromecastVersion = chromecastVersion,
- SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
- ? int.Parse(length, CultureInfo.InvariantCulture)
+ SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength)
+ ? skipForwardLength
: 30000,
- SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
- ? int.Parse(length, CultureInfo.InvariantCulture)
+ SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && !string.IsNullOrEmpty(length) && int.TryParse(length, out var skipBackwardLength)
+ ? skipBackwardLength
: 10000,
- EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
+ EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled)
? bool.Parse(enabled)
: true,
DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 33f039c39..96bd2ccc4 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -1,7 +1,5 @@
using System;
-using System.Globalization;
using System.IO;
-using System.Linq;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Serialization;
using Jellyfin.Data.Entities;
@@ -76,7 +74,7 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var entry in queryResult)
{
- UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
+ UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.Options);
if (mockup == null)
{
continue;
@@ -84,11 +82,14 @@ namespace Jellyfin.Server.Migrations.Routines
var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
- var config = File.Exists(Path.Combine(userDataDir, "config.xml"))
- ? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml"))
+ var configPath = Path.Combine(userDataDir, "config.xml");
+ var config = File.Exists(configPath)
+ ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration()
: new UserConfiguration();
- var policy = File.Exists(Path.Combine(userDataDir, "policy.xml"))
- ? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml"))
+
+ var policyPath = Path.Combine(userDataDir, "policy.xml");
+ var policy = File.Exists(policyPath)
+ ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy()
: new UserPolicy();
policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
"Emby.Server.Implementations.Library",
@@ -104,7 +105,7 @@ namespace Jellyfin.Server.Migrations.Routines
_ => policy.LoginAttemptsBeforeLockout
};
- var user = new User(mockup.Name, policy.AuthenticationProviderId, policy.PasswordResetProviderId)
+ var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
{
Id = entry[1].ReadGuidFromBlob(),
InternalId = entry[0].ToInt64(),
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index f05cdfe9b..3a3d7415b 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -12,12 +12,12 @@ using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
using Emby.Server.Implementations.IO;
-using Jellyfin.Api.Controllers;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -164,6 +164,7 @@ namespace Jellyfin.Server
appPaths,
_loggerFactory,
options,
+ startupConfig,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
@@ -172,7 +173,7 @@ namespace Jellyfin.Server
// If hosting the web client, validate the client content path
if (startupConfig.HostWebClient())
{
- string? webContentPath = appHost.ServerConfigurationManager.ApplicationPaths.WebPath;
+ string? webContentPath = appHost.ConfigurationManager.ApplicationPaths.WebPath;
if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
{
throw new InvalidOperationException(
@@ -198,11 +199,11 @@ namespace Jellyfin.Server
}
catch
{
- _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in system.xml and try again.");
+ _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
throw;
}
- await appHost.RunStartupTasksAsync().ConfigureAwait(false);
+ await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
stopWatch.Stop();
@@ -221,7 +222,15 @@ namespace Jellyfin.Server
}
finally
{
- appHost?.Dispose();
+ _logger.LogInformation("Running query planner optimizations in the database... This might take a while");
+ // Run before disposing the application
+ using var context = new JellyfinDbProvider(appHost.ServiceProvider, appPaths).CreateContext();
+ if (context.Database.IsSqlite())
+ {
+ context.Database.ExecuteSqlRaw("PRAGMA optimize");
+ }
+
+ appHost.Dispose();
}
if (_restartOnShutdown)
@@ -280,7 +289,7 @@ namespace Jellyfin.Server
bool flagged = false;
foreach (IPObject netAdd in addresses)
{
- _logger.LogInformation("Kestrel listening on {0}", netAdd);
+ _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd);
options.Listen(netAdd.Address, appHost.HttpPort);
if (appHost.ListenWithHttps)
{
@@ -622,7 +631,7 @@ namespace Jellyfin.Server
string commandLineArgsString;
if (options.RestartArgs != null)
{
- commandLineArgsString = options.RestartArgs ?? string.Empty;
+ commandLineArgsString = options.RestartArgs;
}
else
{
diff --git a/Jellyfin.Server/Properties/AssemblyInfo.cs b/Jellyfin.Server/Properties/AssemblyInfo.cs
index 7abf298b1..fe2d5c5f9 100644
--- a/Jellyfin.Server/Properties/AssemblyInfo.cs
+++ b/Jellyfin.Server/Properties/AssemblyInfo.cs
@@ -21,4 +21,4 @@ using System.Runtime.InteropServices;
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
-[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")]
+[assembly: InternalsVisibleTo("Jellyfin.Server.Tests")]
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index e56e61092..60cdc2f6f 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,5 +1,9 @@
+using System;
+using System.Net;
+using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
+using System.Text;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Implementations;
@@ -67,6 +71,12 @@ namespace Jellyfin.Server
var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0);
var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9);
var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8);
+ Func<IServiceProvider, HttpMessageHandler> defaultHttpClientHandlerDelegate = (_) => new SocketsHttpHandler()
+ {
+ AutomaticDecompression = DecompressionMethods.All,
+ RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
+ };
+
services
.AddHttpClient(NamedClient.Default, c =>
{
@@ -75,7 +85,7 @@ namespace Jellyfin.Server
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
})
- .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
+ .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
services.AddHttpClient(NamedClient.MusicBrainz, c =>
{
@@ -84,7 +94,7 @@ namespace Jellyfin.Server
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
})
- .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
+ .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
services.AddHealthChecks()
.AddDbContextCheck<JellyfinDb>();
@@ -150,6 +160,7 @@ namespace Jellyfin.Server
mainApp.UseAuthentication();
mainApp.UseJellyfinApiSwagger(_serverConfigurationManager);
+ mainApp.UseQueryStringDecoding();
mainApp.UseRouting();
mainApp.UseAuthorization();
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index b63434092..a1cecc8c6 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -1,10 +1,7 @@
-using System;
using System.Collections.Generic;
using CommandLine;
using Emby.Server.Implementations;
-using Emby.Server.Implementations.EntryPoints;
using Emby.Server.Implementations.Udp;
-using Emby.Server.Implementations.Updates;
using MediaBrowser.Controller.Extensions;
namespace Jellyfin.Server
@@ -77,7 +74,7 @@ namespace Jellyfin.Server
/// <inheritdoc />
[Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")]
- public Uri? PublishedServerUrl { get; set; }
+ public string? PublishedServerUrl { get; set; }
/// <summary>
/// Gets the command line options as a dictionary that can be used in the .NET configuration system.
@@ -94,7 +91,7 @@ namespace Jellyfin.Server
if (PublishedServerUrl != null)
{
- config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl.ToString());
+ config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl);
}
if (FFmpegPath != null)
diff --git a/Jellyfin.sln b/Jellyfin.sln
index 4e6687cce..9fbd9d266 100644
--- a/Jellyfin.sln
+++ b/Jellyfin.sln
@@ -1,4 +1,5 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244
MinimumVisualStudioVersion = 10.0.40219.1
@@ -68,11 +69,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat
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.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Model.Tests", "tests\Jellyfin.Model.Tests\Jellyfin.Model.Tests.csproj", "{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Tests", "tests\Jellyfin.Server.Tests\Jellyfin.Server.Tests.csproj", "{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Integration.Tests", "tests\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj", "{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Providers.Tests", "tests\Jellyfin.Providers.Tests\Jellyfin.Providers.Tests.csproj", "{A964008C-2136-4716-B6CB-B3426C22320A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -188,10 +197,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
- {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.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
@@ -200,6 +205,30 @@ Global
{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
{30922383-D513-4F4D-B890-A940B57FA353}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A964008C-2136-4716-B6CB-B3426C22320A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A964008C-2136-4716-B6CB-B3426C22320A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -211,9 +240,13 @@ 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}
- {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {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}
+ {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {A964008C-2136-4716-B6CB-B3426C22320A} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
diff --git a/MediaBrowser.Common/Configuration/ConfigurationStore.cs b/MediaBrowser.Common/Configuration/ConfigurationStore.cs
index d31d45e4c..050ab1ab5 100644
--- a/MediaBrowser.Common/Configuration/ConfigurationStore.cs
+++ b/MediaBrowser.Common/Configuration/ConfigurationStore.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
namespace MediaBrowser.Common.Configuration
diff --git a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
index 344aecf53..2df87d879 100644
--- a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
+++ b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
index 57c654667..1370e6d79 100644
--- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs
+++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
namespace MediaBrowser.Common.Configuration
{
/// <summary>
diff --git a/MediaBrowser.Common/Crc32.cs b/MediaBrowser.Common/Crc32.cs
new file mode 100644
index 000000000..599eb4c99
--- /dev/null
+++ b/MediaBrowser.Common/Crc32.cs
@@ -0,0 +1,89 @@
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Common
+{
+ public static class Crc32
+ {
+ private static readonly uint[] _crcTable =
+ {
+ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
+ 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
+ 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
+ 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
+ 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
+ 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+ 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
+ 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
+ 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
+ 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
+ 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
+ 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+ 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
+ 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
+ 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
+ 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
+ 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
+ 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+ 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
+ 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
+ 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
+ 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
+ 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
+ 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+ 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
+ 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
+ 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
+ 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
+ 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
+ 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
+ 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
+ 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
+ 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
+ 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
+ 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+ 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
+ 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
+ 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
+ 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
+ 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
+ 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+ 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
+ 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
+ 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
+ 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
+ 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
+ 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+ 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
+ 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
+ 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
+ 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
+ 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
+ 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+ 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
+ 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
+ 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
+ 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
+ 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
+ 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
+ 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
+ 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
+ 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
+ };
+
+ public static uint Compute(ReadOnlySpan<byte> bytes)
+ {
+ var crc = 0xffffffff;
+ var len = bytes.Length;
+ for (var i = 0; i < len; i++)
+ {
+ crc = (crc >> 8) ^ _crcTable[(bytes[i] ^ crc) & 0xff];
+ }
+
+ return ~crc;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs
index 3e2eae1c8..0e2065302 100644
--- a/MediaBrowser.Common/Cryptography/PasswordHash.cs
+++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Text;
namespace MediaBrowser.Common.Cryptography
@@ -30,6 +29,16 @@ namespace MediaBrowser.Common.Cryptography
public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary<string, string> parameters)
{
+ if (id == null)
+ {
+ throw new ArgumentNullException(nameof(id));
+ }
+
+ if (id.Length == 0)
+ {
+ throw new ArgumentException("String can't be empty", nameof(id));
+ }
+
Id = id;
_hash = hash;
_salt = salt;
@@ -59,58 +68,109 @@ namespace MediaBrowser.Common.Cryptography
/// <value>Return the hashed password.</value>
public ReadOnlySpan<byte> Hash => _hash;
- public static PasswordHash Parse(string hashString)
+ public static PasswordHash Parse(ReadOnlySpan<char> hashString)
{
- // The string should at least contain the hash function and the hash itself
- string[] splitted = hashString.Split('$');
- if (splitted.Length < 3)
+ if (hashString.IsEmpty)
+ {
+ throw new ArgumentException("String can't be empty", nameof(hashString));
+ }
+
+ if (hashString[0] != '$')
{
- throw new ArgumentException("String doesn't contain enough segments", nameof(hashString));
+ throw new FormatException("Hash string must start with a $");
}
- // Start at 1, the first index shouldn't contain any data
- int index = 1;
+ // Ignore first $
+ hashString = hashString[1..];
- // Name of the hash function
- string id = splitted[index++];
+ int nextSegment = hashString.IndexOf('$');
+ if (hashString.IsEmpty || nextSegment == 0)
+ {
+ throw new FormatException("Hash string must contain a valid id");
+ }
+ else if (nextSegment == -1)
+ {
+ return new PasswordHash(hashString.ToString(), Array.Empty<byte>());
+ }
+
+ ReadOnlySpan<char> id = hashString[..nextSegment];
+ hashString = hashString[(nextSegment + 1)..];
+ Dictionary<string, string>? parameters = null;
+
+ nextSegment = hashString.IndexOf('$');
// Optional parameters
- Dictionary<string, string> parameters = new Dictionary<string, string>();
- if (splitted[index].IndexOf('=', StringComparison.Ordinal) != -1)
+ ReadOnlySpan<char> parametersSpan = nextSegment == -1 ? hashString : hashString[..nextSegment];
+ if (parametersSpan.Contains('='))
{
- foreach (string paramset in splitted[index++].Split(','))
+ while (!parametersSpan.IsEmpty)
{
- if (string.IsNullOrEmpty(paramset))
+ ReadOnlySpan<char> parameter;
+ int index = parametersSpan.IndexOf(',');
+ if (index == -1)
+ {
+ parameter = parametersSpan;
+ parametersSpan = ReadOnlySpan<char>.Empty;
+ }
+ else
{
- continue;
+ parameter = parametersSpan[..index];
+ parametersSpan = parametersSpan[(index + 1)..];
}
- string[] fields = paramset.Split('=');
- if (fields.Length != 2)
+ int splitIndex = parameter.IndexOf('=');
+ if (splitIndex == -1 || splitIndex == 0 || splitIndex == parameter.Length - 1)
{
- throw new InvalidDataException($"Malformed parameter in password hash string {paramset}");
+ throw new FormatException("Malformed parameter in password hash string");
}
- parameters.Add(fields[0], fields[1]);
+ (parameters ??= new Dictionary<string, string>()).Add(
+ parameter[..splitIndex].ToString(),
+ parameter[(splitIndex + 1)..].ToString());
+ }
+
+ if (nextSegment == -1)
+ {
+ // parameters can't be null here
+ return new PasswordHash(id.ToString(), Array.Empty<byte>(), Array.Empty<byte>(), parameters!);
}
+
+ hashString = hashString[(nextSegment + 1)..];
+ nextSegment = hashString.IndexOf('$');
+ }
+
+ if (nextSegment == 0)
+ {
+ throw new FormatException("Hash string contains an empty segment");
}
byte[] hash;
byte[] salt;
- // Check if the string also contains a salt
- if (splitted.Length - index == 2)
+ if (nextSegment == -1)
{
- salt = Convert.FromHexString(splitted[index++]);
- hash = Convert.FromHexString(splitted[index++]);
+ salt = Array.Empty<byte>();
+ hash = Convert.FromHexString(hashString);
}
else
{
- salt = Array.Empty<byte>();
- hash = Convert.FromHexString(splitted[index++]);
+ salt = Convert.FromHexString(hashString[..nextSegment]);
+ hashString = hashString[(nextSegment + 1)..];
+ nextSegment = hashString.IndexOf('$');
+ if (nextSegment != -1)
+ {
+ throw new FormatException("Hash string contains too many segments");
+ }
+
+ if (hashString.IsEmpty)
+ {
+ throw new FormatException("Hash segment is empty");
+ }
+
+ hash = Convert.FromHexString(hashString);
}
- return new PasswordHash(id, hash, salt, parameters);
+ return new PasswordHash(id.ToString(), hash, salt, parameters ?? new Dictionary<string, string>());
}
private void SerializeParameters(StringBuilder stringBuilder)
@@ -147,8 +207,13 @@ namespace MediaBrowser.Common.Cryptography
.Append(Convert.ToHexString(_salt));
}
- return str.Append('$')
- .Append(Convert.ToHexString(_hash)).ToString();
+ if (_hash.Length != 0)
+ {
+ str.Append('$')
+ .Append(Convert.ToHexString(_hash));
+ }
+
+ return str.ToString();
}
}
}
diff --git a/MediaBrowser.Common/Events/EventHelper.cs b/MediaBrowser.Common/Events/EventHelper.cs
index c9d3226ac..a9cf86fbc 100644
--- a/MediaBrowser.Common/Events/EventHelper.cs
+++ b/MediaBrowser.Common/Events/EventHelper.cs
@@ -17,7 +17,7 @@ namespace MediaBrowser.Common.Events
/// <param name="sender">The sender.</param>
/// <param name="args">The <see cref="EventArgs" /> instance containing the event data.</param>
/// <param name="logger">The logger.</param>
- public static void QueueEventIfNotNull(EventHandler handler, object sender, EventArgs args, ILogger logger)
+ public static void QueueEventIfNotNull(EventHandler? handler, object sender, EventArgs args, ILogger logger)
{
if (handler != null)
{
@@ -43,7 +43,7 @@ namespace MediaBrowser.Common.Events
/// <param name="sender">The sender.</param>
/// <param name="args">The args.</param>
/// <param name="logger">The logger.</param>
- public static void QueueEventIfNotNull<T>(EventHandler<T> handler, object sender, T args, ILogger logger)
+ public static void QueueEventIfNotNull<T>(EventHandler<T>? handler, object sender, T args, ILogger logger)
{
if (handler != null)
{
diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs
index 40020093b..08964420e 100644
--- a/MediaBrowser.Common/Extensions/BaseExtensions.cs
+++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Security.Cryptography;
using System.Text;
diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/MediaBrowser.Common/Extensions/CopyToExtensions.cs
index 94bf7c740..2ecbc6539 100644
--- a/MediaBrowser.Common/Extensions/CopyToExtensions.cs
+++ b/MediaBrowser.Common/Extensions/CopyToExtensions.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System.Collections.Generic;
namespace MediaBrowser.Common.Extensions
diff --git a/MediaBrowser.Common/Extensions/EnumerableExtensions.cs b/MediaBrowser.Common/Extensions/EnumerableExtensions.cs
new file mode 100644
index 000000000..2b8a6c395
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/EnumerableExtensions.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Static extensions for the <see cref="IEnumerable{T}"/> interface.
+ /// </summary>
+ public static class EnumerableExtensions
+ {
+ /// <summary>
+ /// Determines whether the value is contained in the source collection.
+ /// </summary>
+ /// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param>
+ /// <param name="value">The value to look for in the collection.</param>
+ /// <param name="stringComparison">The string comparison.</param>
+ /// <returns>A value indicating whether the value is contained in the collection.</returns>
+ /// <exception cref="ArgumentNullException">The source is null.</exception>
+ public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (source is IList<string> list)
+ {
+ int len = list.Count;
+ for (int i = 0; i < len; i++)
+ {
+ if (value.Equals(list[i], stringComparison))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ foreach (string element in source)
+ {
+ if (value.Equals(element, stringComparison))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
index 19fa95480..1e5877c84 100644
--- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
+++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs
@@ -17,7 +17,7 @@ namespace MediaBrowser.Common.Extensions
{
return (context.Connection.LocalIpAddress == null
&& context.Connection.RemoteIpAddress == null)
- || context.Connection.LocalIpAddress.Equals(context.Connection.RemoteIpAddress);
+ || Equals(context.Connection.LocalIpAddress, context.Connection.RemoteIpAddress);
}
/// <summary>
@@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Extensions
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>The remote caller IP address.</returns>
- public static string GetNormalizedRemoteIp(this HttpContext context)
+ public static IPAddress GetNormalizedRemoteIp(this HttpContext context)
{
// Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
var ip = context.Connection.RemoteIpAddress ?? IPAddress.Loopback;
@@ -35,7 +35,7 @@ namespace MediaBrowser.Common.Extensions
ip = ip.MapToIPv4();
}
- return ip.ToString();
+ return ip;
}
}
}
diff --git a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs
index 258bd6662..48e758ee4 100644
--- a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs
+++ b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
namespace MediaBrowser.Common.Extensions
diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
index 2f52ba196..c74787122 100644
--- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs
+++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Diagnostics;
using System.Threading;
diff --git a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs
index 7c7bdaa92..95802a462 100644
--- a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs
+++ b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
index ebac9d8e6..22130c5a1 100644
--- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
+++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
namespace MediaBrowser.Common.Extensions
diff --git a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs
index 6f0ea9bd5..2604abf85 100644
--- a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs
+++ b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Common/Extensions/SplitStringExtensions.cs b/MediaBrowser.Common/Extensions/SplitStringExtensions.cs
new file mode 100644
index 000000000..9c9108495
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/SplitStringExtensions.cs
@@ -0,0 +1,95 @@
+/*
+MIT License
+
+Copyright (c) 2019 Gérald Barré
+
+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.
+ */
+
+#pragma warning disable CS1591
+#pragma warning disable CA1034
+using System;
+using System.Diagnostics.Contracts;
+using System.Runtime.InteropServices;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Extension class for splitting lines without unnecessary allocations.
+ /// </summary>
+ public static class SplitStringExtensions
+ {
+ /// <summary>
+ /// Creates a new string split enumerator.
+ /// </summary>
+ /// <param name="str">The string to split.</param>
+ /// <param name="separator">The separator to split on.</param>
+ /// <returns>The enumerator struct.</returns>
+ [Pure]
+ public static SplitEnumerator SpanSplit(this string str, char separator) => new (str.AsSpan(), separator);
+
+ /// <summary>
+ /// Creates a new span split enumerator.
+ /// </summary>
+ /// <param name="str">The span to split.</param>
+ /// <param name="separator">The separator to split on.</param>
+ /// <returns>The enumerator struct.</returns>
+ [Pure]
+ public static SplitEnumerator Split(this ReadOnlySpan<char> str, char separator) => new (str, separator);
+
+ [StructLayout(LayoutKind.Auto)]
+ public ref struct SplitEnumerator
+ {
+ private readonly char _separator;
+ private ReadOnlySpan<char> _str;
+
+ public SplitEnumerator(ReadOnlySpan<char> str, char separator)
+ {
+ _str = str;
+ _separator = separator;
+ Current = default;
+ }
+
+ public ReadOnlySpan<char> Current { get; private set; }
+
+ public readonly SplitEnumerator GetEnumerator() => this;
+
+ public bool MoveNext()
+ {
+ if (_str.Length == 0)
+ {
+ return false;
+ }
+
+ var span = _str;
+ var index = span.IndexOf(_separator);
+ if (index == -1)
+ {
+ _str = ReadOnlySpan<char>.Empty;
+ Current = span;
+ return true;
+ }
+
+ Current = span.Slice(0, index);
+ _str = span[(index + 1)..];
+ return true;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/StreamExtensions.cs b/MediaBrowser.Common/Extensions/StreamExtensions.cs
new file mode 100644
index 000000000..5cbf57d98
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/StreamExtensions.cs
@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Class BaseExtensions.
+ /// </summary>
+ public static class StreamExtensions
+ {
+ /// <summary>
+ /// Reads all lines in the <see cref="Stream" />.
+ /// </summary>
+ /// <param name="stream">The <see cref="Stream" /> to read from.</param>
+ /// <returns>All lines in the stream.</returns>
+ public static string[] ReadAllLines(this Stream stream)
+ => ReadAllLines(stream, Encoding.UTF8);
+
+ /// <summary>
+ /// Reads all lines in the <see cref="Stream" />.
+ /// </summary>
+ /// <param name="stream">The <see cref="Stream" /> to read from.</param>
+ /// <param name="encoding">The character encoding to use.</param>
+ /// <returns>All lines in the stream.</returns>
+ public static string[] ReadAllLines(this Stream stream, Encoding encoding)
+ {
+ using (StreamReader reader = new StreamReader(stream, encoding))
+ {
+ return ReadAllLines(reader).ToArray();
+ }
+ }
+
+ /// <summary>
+ /// Reads all lines in the <see cref="TextReader" />.
+ /// </summary>
+ /// <param name="reader">The <see cref="TextReader" /> to read from.</param>
+ /// <returns>All lines in the stream.</returns>
+ public static IEnumerable<string> ReadAllLines(this TextReader reader)
+ {
+ string? line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ yield return line;
+ }
+ }
+
+ /// <summary>
+ /// Reads all lines in the <see cref="TextReader" />.
+ /// </summary>
+ /// <param name="reader">The <see cref="TextReader" /> to read from.</param>
+ /// <returns>All lines in the stream.</returns>
+ public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader)
+ {
+ string? line;
+ while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
+ {
+ yield return line;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs
new file mode 100644
index 000000000..75d654f23
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Extension methods for the <see cref="StringBuilder"/> class.
+ /// </summary>
+ public static class StringBuilderExtensions
+ {
+ /// <summary>
+ /// Concatenates and appends the members of a collection in single quotes using the specified delimiter.
+ /// </summary>
+ /// <param name="builder">The string builder.</param>
+ /// <param name="delimiter">The character delimiter.</param>
+ /// <param name="values">The collection of strings to concatenate.</param>
+ /// <returns>The updated string builder.</returns>
+ public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList<string> values)
+ {
+ var len = values.Count;
+ for (var i = 0; i < len; i++)
+ {
+ builder.Append('\'')
+ .Append(values[i])
+ .Append('\'')
+ .Append(delimiter);
+ }
+
+ // remove last ,
+ builder.Length--;
+
+ return builder;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/StringExtensions.cs b/MediaBrowser.Common/Extensions/StringExtensions.cs
deleted file mode 100644
index 764301741..000000000
--- a/MediaBrowser.Common/Extensions/StringExtensions.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-#nullable enable
-
-using System;
-
-namespace MediaBrowser.Common.Extensions
-{
- /// <summary>
- /// Extensions methods to simplify string operations.
- /// </summary>
- public static class StringExtensions
- {
- /// <summary>
- /// Returns the part on the left of the <c>needle</c>.
- /// </summary>
- /// <param name="haystack">The string to seek.</param>
- /// <param name="needle">The needle to find.</param>
- /// <returns>The part left of the <paramref name="needle" />.</returns>
- public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, char needle)
- {
- var pos = haystack.IndexOf(needle);
- return pos == -1 ? haystack : haystack[..pos];
- }
-
- /// <summary>
- /// Returns the part on the left of the <c>needle</c>.
- /// </summary>
- /// <param name="haystack">The string to seek.</param>
- /// <param name="needle">The needle to find.</param>
- /// <param name="stringComparison">One of the enumeration values that specifies the rules for the search.</param>
- /// <returns>The part left of the <c>needle</c>.</returns>
- public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle, StringComparison stringComparison = default)
- {
- var pos = haystack.IndexOf(needle, stringComparison);
- return pos == -1 ? haystack : haystack[..pos];
- }
- }
-}
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index ddcf2ac17..192a77611 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -10,7 +10,7 @@ namespace MediaBrowser.Common
/// </summary>
/// <param name="type">Type to create.</param>
/// <returns>New instance of type <param>type</param>.</returns>
- public delegate object CreationDelegate(Type type);
+ public delegate object? CreationDelegateFactory(Type type);
/// <summary>
/// An interface to be implemented by the applications hosting a kernel.
@@ -20,7 +20,7 @@ namespace MediaBrowser.Common
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
- event EventHandler HasPendingRestartChanged;
+ event EventHandler? HasPendingRestartChanged;
/// <summary>
/// Gets the name.
@@ -61,7 +61,7 @@ namespace MediaBrowser.Common
/// <summary>
/// Gets or sets the service provider.
/// </summary>
- IServiceProvider ServiceProvider { get; set; }
+ IServiceProvider? ServiceProvider { get; set; }
/// <summary>
/// Gets the application version.
@@ -112,7 +112,7 @@ namespace MediaBrowser.Common
/// <param name="defaultFunc">Delegate function that gets called to create the object.</param>
/// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
- IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true);
+ IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true);
/// <summary>
/// Gets the export types.
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
index 38a7e1d20..127a41a06 100644
--- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
@@ -9,67 +9,16 @@ namespace MediaBrowser.Common.Json.Converters
/// Convert comma delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
- public class JsonCommaDelimitedArrayConverter<T> : JsonConverter<T[]>
+ public sealed class JsonCommaDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T>
{
- private readonly TypeConverter _typeConverter;
-
/// <summary>
/// Initializes a new instance of the <see cref="JsonCommaDelimitedArrayConverter{T}"/> class.
/// </summary>
- public JsonCommaDelimitedArrayConverter()
+ public JsonCommaDelimitedArrayConverter() : base()
{
- _typeConverter = TypeDescriptor.GetConverter(typeof(T));
}
/// <inheritdoc />
- public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- if (reader.TokenType == JsonTokenType.String)
- {
- var stringEntries = reader.GetString()?.Split(',', StringSplitOptions.RemoveEmptyEntries);
- if (stringEntries == null || stringEntries.Length == 0)
- {
- return Array.Empty<T>();
- }
-
- var parsedValues = new object[stringEntries.Length];
- var convertedCount = 0;
- for (var i = 0; i < stringEntries.Length; i++)
- {
- try
- {
- parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
- convertedCount++;
- }
- catch (FormatException)
- {
- // TODO log when upgraded to .Net6
- // https://github.com/dotnet/runtime/issues/42975
- // _logger.LogDebug(e, "Error converting value.");
- }
- }
-
- var typedValues = new T[convertedCount];
- var typedValueIndex = 0;
- for (var i = 0; i < stringEntries.Length; i++)
- {
- if (parsedValues[i] != null)
- {
- typedValues.SetValue(parsedValues[i], typedValueIndex);
- typedValueIndex++;
- }
- }
-
- return typedValues;
- }
-
- return JsonSerializer.Deserialize<T[]>(ref reader, options);
- }
-
- /// <inheritdoc />
- public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
- {
- JsonSerializer.Serialize(writer, value, options);
- }
+ protected override char Delimiter => ',';
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
index 24ed3ea19..de41348dd 100644
--- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
@@ -19,10 +19,10 @@ namespace MediaBrowser.Common.Json.Converters
}
/// <inheritdoc />
- public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
- return (JsonConverter)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
+ return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs
new file mode 100644
index 000000000..b691798c9
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -0,0 +1,81 @@
+using System;
+using System.ComponentModel;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Convert delimited string to array of type.
+ /// </summary>
+ /// <typeparam name="T">Type to convert to.</typeparam>
+ public abstract class JsonDelimitedArrayConverter<T> : JsonConverter<T[]?>
+ {
+ private readonly TypeConverter _typeConverter;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JsonDelimitedArrayConverter{T}"/> class.
+ /// </summary>
+ protected JsonDelimitedArrayConverter()
+ {
+ _typeConverter = TypeDescriptor.GetConverter(typeof(T));
+ }
+
+ /// <summary>
+ /// Gets the array delimiter.
+ /// </summary>
+ protected virtual char Delimiter { get; }
+
+ /// <inheritdoc />
+ public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ // GetString can't return null here because we already handled it above
+ var stringEntries = reader.GetString()?.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
+ if (stringEntries == null || stringEntries.Length == 0)
+ {
+ return Array.Empty<T>();
+ }
+
+ var parsedValues = new object[stringEntries.Length];
+ var convertedCount = 0;
+ for (var i = 0; i < stringEntries.Length; i++)
+ {
+ try
+ {
+ parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
+ convertedCount++;
+ }
+ catch (FormatException)
+ {
+ // TODO log when upgraded to .Net6
+ // https://github.com/dotnet/runtime/issues/42975
+ // _logger.LogDebug(e, "Error converting value.");
+ }
+ }
+
+ var typedValues = new T[convertedCount];
+ var typedValueIndex = 0;
+ for (var i = 0; i < stringEntries.Length; i++)
+ {
+ if (parsedValues[i] != null)
+ {
+ typedValues.SetValue(parsedValues[i], typedValueIndex);
+ typedValueIndex++;
+ }
+ }
+
+ return typedValues;
+ }
+
+ return JsonSerializer.Deserialize<T[]>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
index d5b54e3ca..e2a3d798a 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
@@ -18,10 +18,10 @@ namespace MediaBrowser.Common.Json.Converters
}
/// <inheritdoc />
- public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GenericTypeArguments[0];
- return (JsonConverter)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType));
+ return (JsonConverter?)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType));
}
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs
index cb3d83f58..3d97a9de5 100644
--- a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Json.Converters
return (int?)converter.ConvertFromString(str);
}
- return JsonSerializer.Deserialize<int?>(ref reader, options);
+ return JsonSerializer.Deserialize<int>(ref reader, options);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs
index 6a8790374..77cf46b70 100644
--- a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs
@@ -7,15 +7,21 @@ namespace MediaBrowser.Common.Json.Converters
/// <summary>
/// Converts a string <c>N/A</c> to <c>string.Empty</c>.
/// </summary>
- public class JsonOmdbNotAvailableStringConverter : JsonConverter<string>
+ public class JsonOmdbNotAvailableStringConverter : JsonConverter<string?>
{
/// <inheritdoc />
- public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
if (reader.TokenType == JsonTokenType.String)
{
- var str = reader.GetString();
- if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ // GetString can't return null here because we already handled it above
+ var str = reader.GetString()!;
+ if (str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -23,11 +29,11 @@ namespace MediaBrowser.Common.Json.Converters
return str;
}
- return JsonSerializer.Deserialize<string>(ref reader, options);
+ return JsonSerializer.Deserialize<string?>(ref reader, options);
}
/// <inheritdoc />
- public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
index 377db1a44..a8f6cfbec 100644
--- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
@@ -9,67 +9,16 @@ namespace MediaBrowser.Common.Json.Converters
/// Convert Pipe delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
- public class JsonPipeDelimitedArrayConverter<T> : JsonConverter<T[]>
+ public sealed class JsonPipeDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T>
{
- private readonly TypeConverter _typeConverter;
-
/// <summary>
/// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class.
/// </summary>
- public JsonPipeDelimitedArrayConverter()
+ public JsonPipeDelimitedArrayConverter() : base()
{
- _typeConverter = TypeDescriptor.GetConverter(typeof(T));
}
/// <inheritdoc />
- public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- if (reader.TokenType == JsonTokenType.String)
- {
- var stringEntries = reader.GetString()?.Split('|', StringSplitOptions.RemoveEmptyEntries);
- if (stringEntries == null || stringEntries.Length == 0)
- {
- return Array.Empty<T>();
- }
-
- var parsedValues = new object[stringEntries.Length];
- var convertedCount = 0;
- for (var i = 0; i < stringEntries.Length; i++)
- {
- try
- {
- parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
- convertedCount++;
- }
- catch (FormatException)
- {
- // TODO log when upgraded to .Net6
- // https://github.com/dotnet/runtime/issues/42975
- // _logger.LogDebug(e, "Error converting value.");
- }
- }
-
- var typedValues = new T[convertedCount];
- var typedValueIndex = 0;
- for (var i = 0; i < stringEntries.Length; i++)
- {
- if (parsedValues[i] != null)
- {
- typedValues.SetValue(parsedValues[i], typedValueIndex);
- typedValueIndex++;
- }
- }
-
- return typedValues;
- }
-
- return JsonSerializer.Deserialize<T[]>(ref reader, options);
- }
-
- /// <inheritdoc />
- public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
- {
- JsonSerializer.Serialize(writer, value, options);
- }
+ protected override char Delimiter => '|';
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
index 5e77223ef..1bebc49ec 100644
--- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
@@ -19,10 +19,10 @@ namespace MediaBrowser.Common.Json.Converters
}
/// <inheritdoc />
- public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
- return (JsonConverter)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType));
+ return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType));
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs
new file mode 100644
index 000000000..6cd980e48
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Buffers;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ /// <summary>
+ /// Converter to allow the serializer to read strings.
+ /// </summary>
+ public class JsonStringConverter : JsonConverter<string?>
+ {
+ /// <inheritdoc />
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return reader.TokenType switch
+ {
+ JsonTokenType.Null => null,
+ JsonTokenType.String => reader.GetString(),
+ _ => GetRawValue(reader)
+ };
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value);
+ }
+
+ private static string GetRawValue(Utf8JsonReader reader)
+ {
+ var utf8Bytes = reader.HasValueSequence
+ ? reader.ValueSequence.ToArray()
+ : reader.ValueSpan;
+ return Encoding.UTF8.GetString(utf8Bytes);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs
index 37e6f64e3..81c093c54 100644
--- a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs
@@ -7,11 +7,14 @@ namespace MediaBrowser.Common.Json.Converters
/// <summary>
/// Converts a Version object or value to/from JSON.
/// </summary>
+ /// <remarks>
+ /// Required to send <see cref="Version"/> as a string instead of an object.
+ /// </remarks>
public class JsonVersionConverter : JsonConverter<Version>
{
/// <inheritdoc />
public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- => new Version(reader.GetString());
+ => new Version(reader.GetString()!); // Will throw ArgumentNullException on null
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 2ef24a884..405d6125f 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -39,7 +39,8 @@ namespace MediaBrowser.Common.Json
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
new JsonBoolNumberConverter(),
- new JsonDateTimeConverter()
+ new JsonDateTimeConverter(),
+ new JsonStringConverter()
}
};
@@ -61,7 +62,7 @@ namespace MediaBrowser.Common.Json
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
- public static JsonSerializerOptions GetOptions()
+ public static JsonSerializerOptions Options
=> _jsonSerializerOptions;
/// <summary>
@@ -72,7 +73,7 @@ namespace MediaBrowser.Common.Json
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns>
- public static JsonSerializerOptions GetCamelCaseOptions()
+ public static JsonSerializerOptions CamelCaseOptions
=> _camelCaseJsonSerializerOptions;
/// <summary>
@@ -83,7 +84,7 @@ namespace MediaBrowser.Common.Json
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns>
- public static JsonSerializerOptions GetPascalCaseOptions()
+ public static JsonSerializerOptions PascalCaseOptions
=> _pascalCaseJsonSerializerOptions;
}
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index e469436a9..0299a8456 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
- <FrameworkReference Include="Microsoft.AspNetCore.App"/>
+ <FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
</ItemGroup>
@@ -33,6 +33,9 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
@@ -46,16 +49,11 @@
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Jellyfin.Common.Tests</_Parameter1>
diff --git a/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs b/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs
deleted file mode 100644
index f1c5f2477..000000000
--- a/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Net;
-using System.Net.Http;
-
-namespace MediaBrowser.Common.Net
-{
- /// <summary>
- /// Default http client handler.
- /// </summary>
- public class DefaultHttpClientHandler : HttpClientHandler
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="DefaultHttpClientHandler"/> class.
- /// </summary>
- public DefaultHttpClientHandler()
- {
- AutomaticDecompression = DecompressionMethods.All;
- }
- }
-}
diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs
index b6c390d23..b93939730 100644
--- a/MediaBrowser.Common/Net/INetworkManager.cs
+++ b/MediaBrowser.Common/Net/INetworkManager.cs
@@ -1,10 +1,8 @@
-#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net;
using System.Net.NetworkInformation;
-using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Common.Net
@@ -229,5 +227,12 @@ namespace MediaBrowser.Common.Net
/// <param name="filter">Optional filter for the list.</param>
/// <returns>Returns a filtered list of LAN addresses.</returns>
Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null);
+
+ /// <summary>
+ /// Checks to see if <paramref name="remoteIp"/> has access.
+ /// </summary>
+ /// <param name="remoteIp">IP Address of client.</param>
+ /// <returns><b>True</b> if has access, otherwise <b>false</b>.</returns>
+ bool HasRemoteAccess(IPAddress remoteIp);
}
}
diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs
index 4cede9ab1..5db8817ee 100644
--- a/MediaBrowser.Common/Net/IPHost.cs
+++ b/MediaBrowser.Common/Net/IPHost.cs
@@ -1,4 +1,3 @@
-#nullable enable
using System;
using System.Diagnostics;
using System.Linq;
@@ -128,62 +127,63 @@ namespace MediaBrowser.Common.Net
/// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
public static bool TryParse(string host, out IPHost hostObj)
{
- if (!string.IsNullOrEmpty(host))
+ if (string.IsNullOrWhiteSpace(host))
{
- // See if it's an IPv6 with port address e.g. [::1]:120.
- int i = host.IndexOf("]:", StringComparison.OrdinalIgnoreCase);
- if (i != -1)
- {
- return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
- }
- else
- {
- // See if it's an IPv6 in [] with no port.
- i = host.IndexOf(']', StringComparison.OrdinalIgnoreCase);
- if (i != -1)
- {
- return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
- }
+ hostObj = IPHost.None;
+ return false;
+ }
- // Is it a host or IPv4 with port?
- string[] hosts = host.Split(':');
+ // See if it's an IPv6 with port address e.g. [::1] or [::1]:120.
+ int i = host.IndexOf(']', StringComparison.Ordinal);
+ if (i != -1)
+ {
+ return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
+ }
- if (hosts.Length > 2)
- {
- hostObj = new IPHost(string.Empty, IPAddress.None);
- return false;
- }
+ if (IPNetAddress.TryParse(host, out var netAddress))
+ {
+ // Host name is an ip address, so fake resolve.
+ hostObj = new IPHost(host, netAddress.Address);
+ return true;
+ }
- // Remove port from IPv4 if it exists.
- host = hosts[0];
+ // Is it a host, IPv4/6 with/out port?
+ string[] hosts = host.Split(':');
- if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase))
- {
- hostObj = new IPHost(host, new IPAddress(Ipv4Loopback));
- return true;
- }
+ if (hosts.Length <= 2)
+ {
+ // This is either a hostname: port, or an IP4:port.
+ host = hosts[0];
- if (IPNetAddress.TryParse(host, out IPNetAddress netIP))
- {
- // Host name is an ip address, so fake resolve.
- hostObj = new IPHost(host, netIP.Address);
- return true;
- }
+ if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase))
+ {
+ hostObj = new IPHost(host);
+ return true;
}
- // Only thing left is to see if it's a host string.
- if (!string.IsNullOrEmpty(host))
+ if (IPAddress.TryParse(host, out var netIP))
{
- // 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
- Regex re = new Regex(@"^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline);
- if (re.Match(host).Success)
- {
- hostObj = new IPHost(host);
- return true;
- }
+ // Host name is an ip address, so fake resolve.
+ hostObj = new IPHost(host, netIP);
+ return true;
}
}
+ else
+ {
+ // Invalid host name, as it cannot contain :
+ hostObj = new IPHost(string.Empty, IPAddress.None);
+ return false;
+ }
+
+ // 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
+ string pattern = @"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$";
+
+ if (Regex.IsMatch(host, pattern))
+ {
+ hostObj = new IPHost(host);
+ return true;
+ }
hostObj = IPHost.None;
return false;
@@ -344,10 +344,14 @@ namespace MediaBrowser.Common.Net
{
output += "Any Address,";
}
- else
+ else if (i.AddressFamily == AddressFamily.InterNetwork)
{
output += $"{i}/32,";
}
+ else
+ {
+ output += $"{i}/128,";
+ }
}
output = output[0..^1];
@@ -384,8 +388,8 @@ namespace MediaBrowser.Common.Net
/// <inheritdoc/>
protected override IPObject CalculateNetworkAddress()
{
- var netAddr = NetworkAddressOf(this[0], PrefixLength);
- return new IPNetAddress(netAddr.Address, netAddr.PrefixLength);
+ var (address, prefixLength) = NetworkAddressOf(this[0], PrefixLength);
+ return new IPNetAddress(address, prefixLength);
}
/// <summary>
@@ -395,13 +399,10 @@ namespace MediaBrowser.Common.Net
private bool ResolveHost()
{
// When was the last time we resolved?
- if (_lastResolved == null)
- {
- _lastResolved = DateTime.UtcNow;
- }
+ _lastResolved ??= DateTime.UtcNow;
// If we haven't resolved before, or our timer has run out...
- if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved?.AddMinutes(Timeout)))
+ if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved.Value.AddMinutes(Timeout)))
{
_lastResolved = DateTime.UtcNow;
ResolveHostInternal().GetAwaiter().GetResult();
@@ -422,7 +423,7 @@ namespace MediaBrowser.Common.Net
// Resolves the host name - so save a DNS lookup.
if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase))
{
- _addresses = new IPAddress[] { new IPAddress(Ipv4Loopback), new IPAddress(Ipv6Loopback) };
+ _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback };
return;
}
diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs
index 5fab52eac..f6e3971bf 100644
--- a/MediaBrowser.Common/Net/IPNetAddress.cs
+++ b/MediaBrowser.Common/Net/IPNetAddress.cs
@@ -1,4 +1,3 @@
-#nullable enable
using System;
using System.Net;
using System.Net.Sockets;
@@ -38,7 +37,7 @@ namespace MediaBrowser.Common.Net
/// <summary>
/// IP6Loopback address host.
/// </summary>
- public static readonly IPNetAddress IP6Loopback = IPNetAddress.Parse("::1");
+ public static readonly IPNetAddress IP6Loopback = new IPNetAddress(IPAddress.IPv6Loopback);
/// <summary>
/// Object's IP address.
@@ -113,7 +112,7 @@ namespace MediaBrowser.Common.Net
}
// Is it a network?
- string[] tokens = addr.Split("/");
+ string[] tokens = addr.Split('/');
if (tokens.Length == 2)
{
@@ -171,8 +170,8 @@ namespace MediaBrowser.Common.Net
address = address.MapToIPv4();
}
- var altAddress = NetworkAddressOf(address, PrefixLength);
- return NetworkAddress.Address.Equals(altAddress.Address) && NetworkAddress.PrefixLength >= altAddress.PrefixLength;
+ var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength);
+ return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix;
}
/// <inheritdoc/>
@@ -196,8 +195,8 @@ namespace MediaBrowser.Common.Net
return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength;
}
- var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength);
- return NetworkAddress.Address.Equals(altAddress.Address);
+ var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength).address;
+ return NetworkAddress.Address.Equals(altAddress);
}
return false;
@@ -216,11 +215,11 @@ namespace MediaBrowser.Common.Net
}
/// <inheritdoc/>
- public override bool Equals(IPAddress address)
+ public override bool Equals(IPAddress ip)
{
- if (address != null && !address.Equals(IPAddress.None) && !Address.Equals(IPAddress.None))
+ if (ip != null && !ip.Equals(IPAddress.None) && !Address.Equals(IPAddress.None))
{
- return address.Equals(Address);
+ return ip.Equals(Address);
}
return false;
@@ -270,8 +269,8 @@ namespace MediaBrowser.Common.Net
/// <inheritdoc/>
protected override IPObject CalculateNetworkAddress()
{
- var value = NetworkAddressOf(_address, PrefixLength);
- return new IPNetAddress(value.Address, value.PrefixLength);
+ var (address, prefixLength) = NetworkAddressOf(_address, PrefixLength);
+ return new IPNetAddress(address, prefixLength);
}
}
}
diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs
index 69cd57f8a..2612268fd 100644
--- a/MediaBrowser.Common/Net/IPObject.cs
+++ b/MediaBrowser.Common/Net/IPObject.cs
@@ -1,4 +1,3 @@
-#nullable enable
using System;
using System.Net;
using System.Net.Sockets;
@@ -11,16 +10,6 @@ namespace MediaBrowser.Common.Net
public abstract class IPObject : IEquatable<IPObject>
{
/// <summary>
- /// IPv6 Loopback address.
- /// </summary>
- protected static readonly byte[] Ipv6Loopback = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
-
- /// <summary>
- /// IPv4 Loopback address.
- /// </summary>
- protected static readonly byte[] Ipv4Loopback = { 127, 0, 0, 1 };
-
- /// <summary>
/// The network address of this object.
/// </summary>
private IPObject? _networkAddress;
@@ -64,7 +53,7 @@ namespace MediaBrowser.Common.Net
/// <param name="address">IP Address to convert.</param>
/// <param name="prefixLength">Subnet prefix.</param>
/// <returns>IPAddress.</returns>
- public static (IPAddress Address, byte PrefixLength) NetworkAddressOf(IPAddress address, byte prefixLength)
+ public static (IPAddress address, byte prefixLength) NetworkAddressOf(IPAddress address, byte prefixLength)
{
if (address == null)
{
@@ -78,7 +67,7 @@ namespace MediaBrowser.Common.Net
if (IsLoopback(address))
{
- return (Address: address, PrefixLength: prefixLength);
+ return (address, prefixLength);
}
// An ip address is just a list of bytes, each one representing a segment on the network.
@@ -110,7 +99,7 @@ namespace MediaBrowser.Common.Net
}
// Return the network address for the prefix.
- return (Address: new IPAddress(addressBytes), PrefixLength: prefixLength);
+ return (new IPAddress(addressBytes), prefixLength);
}
/// <summary>
diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs
index d07bba249..264bfacb4 100644
--- a/MediaBrowser.Common/Net/NetworkExtensions.cs
+++ b/MediaBrowser.Common/Net/NetworkExtensions.cs
@@ -1,11 +1,6 @@
-#pragma warning disable CA1062 // Validate arguments of public methods
using System;
-using System.Collections;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net;
-using System.Runtime.CompilerServices;
-using System.Text;
namespace MediaBrowser.Common.Net
{
@@ -32,9 +27,11 @@ namespace MediaBrowser.Common.Net
/// </summary>
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
/// <param name="item">Item to add.</param>
- public static void AddItem(this Collection<IPObject> source, IPObject item)
+ /// <param name="itemsAreNetworks">If <c>true</c> the values are treated as subnets.
+ /// If <b>false</b> items are addresses.</param>
+ public static void AddItem(this Collection<IPObject> source, IPObject item, bool itemsAreNetworks = true)
{
- if (!source.ContainsAddress(item))
+ if (!source.ContainsAddress(item) || !itemsAreNetworks)
{
source.Add(item);
}
@@ -195,8 +192,9 @@ namespace MediaBrowser.Common.Net
/// </summary>
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
/// <param name="excludeList">Items to exclude.</param>
+ /// <param name="isNetwork">Collection is a network collection.</param>
/// <returns>A new collection, with the items excluded.</returns>
- public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList)
+ public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList, bool isNetwork)
{
if (source.Count == 0 || excludeList == null)
{
@@ -221,7 +219,7 @@ namespace MediaBrowser.Common.Net
if (!found)
{
- results.AddItem(outer);
+ results.AddItem(outer, isNetwork);
}
}
@@ -234,7 +232,7 @@ namespace MediaBrowser.Common.Net
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
/// <param name="target">Collection to compare with.</param>
/// <returns>A collection containing all the matches.</returns>
- public static Collection<IPObject> Union(this Collection<IPObject> source, Collection<IPObject> target)
+ public static Collection<IPObject> ThatAreContainedInNetworks(this Collection<IPObject> source, Collection<IPObject> target)
{
if (source.Count == 0)
{
diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs
index e228ae7ec..8972089a8 100644
--- a/MediaBrowser.Common/Plugins/BasePlugin.cs
+++ b/MediaBrowser.Common/Plugins/BasePlugin.cs
@@ -1,10 +1,9 @@
+#nullable disable
+
using System;
using System.IO;
using System.Reflection;
-using System.Runtime.InteropServices;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Common.Plugins
{
@@ -53,7 +52,7 @@ namespace MediaBrowser.Common.Plugins
/// Gets a value indicating whether the plugin can be uninstalled.
/// </summary>
public bool CanUninstall => !Path.GetDirectoryName(AssemblyFilePath)
- .Equals(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), StringComparison.InvariantCulture);
+ .Equals(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), StringComparison.Ordinal);
/// <summary>
/// Gets the plugin info.
diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
index d5c780851..8a6d28e0f 100644
--- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs
+++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
@@ -1,4 +1,6 @@
+#nullable disable
#pragma warning disable SA1649 // File name should match first type name
+
using System;
using System.IO;
using System.Runtime.InteropServices;
@@ -39,29 +41,27 @@ namespace MediaBrowser.Common.Plugins
{
ApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
- if (this is IPluginAssembly assemblyPlugin)
- {
- var assembly = GetType().Assembly;
- var assemblyName = assembly.GetName();
- var assemblyFilePath = assembly.Location;
- var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
- if (!Directory.Exists(dataFolderPath) && Version != null)
- {
- // Try again with the version number appended to the folder name.
- dataFolderPath = dataFolderPath + "_" + Version.ToString();
- }
+ var assembly = GetType().Assembly;
+ var assemblyName = assembly.GetName();
+ var assemblyFilePath = assembly.Location;
+
+ var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
+ if (!Directory.Exists(dataFolderPath) && Version != null)
+ {
+ // Try again with the version number appended to the folder name.
+ dataFolderPath = dataFolderPath + "_" + Version.ToString();
+ }
- assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
+ SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
- var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
- if (idAttributes.Length > 0)
- {
- var attribute = (GuidAttribute)idAttributes[0];
- var assemblyId = new Guid(attribute.Value);
+ var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
+ if (idAttributes.Length > 0)
+ {
+ var attribute = (GuidAttribute)idAttributes[0];
+ var assemblyId = new Guid(attribute.Value);
- assemblyPlugin.SetId(assemblyId);
- }
+ SetId(assemblyId);
}
}
@@ -107,10 +107,7 @@ namespace MediaBrowser.Common.Plugins
{
lock (_configurationSyncLock)
{
- if (_configuration == null)
- {
- _configuration = LoadConfiguration();
- }
+ _configuration ??= LoadConfiguration();
}
}
diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs
index b2ba1179c..01e0a536d 100644
--- a/MediaBrowser.Common/Plugins/IPlugin.cs
+++ b/MediaBrowser.Common/Plugins/IPlugin.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Model.Plugins;
diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs
index 3da34d8bb..176bcbbd5 100644
--- a/MediaBrowser.Common/Plugins/IPluginManager.cs
+++ b/MediaBrowser.Common/Plugins/IPluginManager.cs
@@ -1,9 +1,9 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
@@ -16,7 +16,7 @@ namespace MediaBrowser.Common.Plugins
/// <summary>
/// Gets the Plugins.
/// </summary>
- IList<LocalPlugin> Plugins { get; }
+ IReadOnlyList<LocalPlugin> Plugins { get; }
/// <summary>
/// Creates the plugins.
@@ -45,6 +45,16 @@ namespace MediaBrowser.Common.Plugins
bool SaveManifest(PluginManifest manifest, string path);
/// <summary>
+ /// Generates a manifest from repository data.
+ /// </summary>
+ /// <param name="packageInfo">The <see cref="PackageInfo"/> used to generate a manifest.</param>
+ /// <param name="version">Version to be installed.</param>
+ /// <param name="path">The path where to save the manifest.</param>
+ /// <param name="status">Initial status of the plugin.</param>
+ /// <returns>True if successful.</returns>
+ Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status);
+
+ /// <summary>
/// Imports plugin details from a folder.
/// </summary>
/// <param name="folder">Folder of the plugin.</param>
diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs
index 23b6cfa81..4c8e2d504 100644
--- a/MediaBrowser.Common/Plugins/LocalPlugin.cs
+++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs
@@ -1,4 +1,3 @@
-#nullable enable
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Plugins;
@@ -22,7 +21,7 @@ namespace MediaBrowser.Common.Plugins
public LocalPlugin(string path, bool isSupported, PluginManifest manifest)
{
Path = path;
- DllFiles = new List<string>();
+ DllFiles = Array.Empty<string>();
_supported = isSupported;
Manifest = manifest;
}
@@ -59,9 +58,9 @@ namespace MediaBrowser.Common.Plugins
public string Path { get; }
/// <summary>
- /// Gets the list of dll files for this plugin.
+ /// Gets or sets the list of dll files for this plugin.
/// </summary>
- public List<string> DllFiles { get; }
+ public IReadOnlyList<string> DllFiles { get; set; }
/// <summary>
/// Gets or sets the instance of this plugin.
diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs
index 4c724f694..2910dbe14 100644
--- a/MediaBrowser.Common/Plugins/PluginManifest.cs
+++ b/MediaBrowser.Common/Plugins/PluginManifest.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Plugins;
diff --git a/MediaBrowser.Common/Progress/ActionableProgress.cs b/MediaBrowser.Common/Progress/ActionableProgress.cs
index d5bcd5be9..0ba46ea3b 100644
--- a/MediaBrowser.Common/Progress/ActionableProgress.cs
+++ b/MediaBrowser.Common/Progress/ActionableProgress.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable CA1003
using System;
@@ -13,9 +14,9 @@ namespace MediaBrowser.Common.Progress
/// <summary>
/// The _actions.
/// </summary>
- private Action<T> _action;
+ private Action<T>? _action;
- public event EventHandler<T> ProgressChanged;
+ public event EventHandler<T>? ProgressChanged;
/// <summary>
/// Registers the action.
diff --git a/MediaBrowser.Common/Progress/SimpleProgress.cs b/MediaBrowser.Common/Progress/SimpleProgress.cs
index d75675bf1..7071f2bc3 100644
--- a/MediaBrowser.Common/Progress/SimpleProgress.cs
+++ b/MediaBrowser.Common/Progress/SimpleProgress.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable CA1003
using System;
@@ -6,7 +7,7 @@ namespace MediaBrowser.Common.Progress
{
public class SimpleProgress<T> : IProgress<T>
{
- public event EventHandler<T> ProgressChanged;
+ public event EventHandler<T>? ProgressChanged;
public void Report(T value)
{
diff --git a/MediaBrowser.Common/Providers/ProviderIdParsers.cs b/MediaBrowser.Common/Providers/ProviderIdParsers.cs
new file mode 100644
index 000000000..33d09ed38
--- /dev/null
+++ b/MediaBrowser.Common/Providers/ProviderIdParsers.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace MediaBrowser.Common.Providers
+{
+ /// <summary>
+ /// Parsers for provider ids.
+ /// </summary>
+ public static class ProviderIdParsers
+ {
+ private const int ImdbMinNumbers = 7;
+ private const int ImdbMaxNumbers = 8;
+ private const string ImdbPrefix = "tt";
+
+ /// <summary>
+ /// Parses an IMDb id from a string.
+ /// </summary>
+ /// <param name="text">The text to parse.</param>
+ /// <param name="imdbId">The parsed IMDb id.</param>
+ /// <returns>True if parsing was successful, false otherwise.</returns>
+ public static bool TryFindImdbId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> imdbId)
+ {
+ // imdb id is at least 9 chars (tt + 7 numbers)
+ while (text.Length >= 2 + ImdbMinNumbers)
+ {
+ var ttPos = text.IndexOf(ImdbPrefix);
+ if (ttPos == -1)
+ {
+ imdbId = default;
+ return false;
+ }
+
+ text = text.Slice(ttPos);
+ var i = 2;
+ var limit = Math.Min(text.Length, ImdbMaxNumbers + 2);
+ for (; i < limit; i++)
+ {
+ var c = text[i];
+ if (!IsDigit(c))
+ {
+ break;
+ }
+ }
+
+ // skip if more than 8 digits + 2 chars for tt
+ if (i <= ImdbMaxNumbers + 2 && i >= ImdbMinNumbers + 2)
+ {
+ imdbId = text.Slice(0, i);
+ return true;
+ }
+
+ text = text.Slice(i);
+ }
+
+ imdbId = default;
+ return false;
+ }
+
+ /// <summary>
+ /// Parses an TMDb id from a movie url.
+ /// </summary>
+ /// <param name="text">The text with the url to parse.</param>
+ /// <param name="tmdbId">The parsed TMDb id.</param>
+ /// <returns>True if parsing was successful, false otherwise.</returns>
+ public static bool TryFindTmdbMovieId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tmdbId)
+ => TryFindProviderId(text, "themoviedb.org/movie/", out tmdbId);
+
+ /// <summary>
+ /// Parses an TMDb id from a series url.
+ /// </summary>
+ /// <param name="text">The text with the url to parse.</param>
+ /// <param name="tmdbId">The parsed TMDb id.</param>
+ /// <returns>True if parsing was successful, false otherwise.</returns>
+ public static bool TryFindTmdbSeriesId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tmdbId)
+ => TryFindProviderId(text, "themoviedb.org/tv/", out tmdbId);
+
+ /// <summary>
+ /// Parses an TVDb id from a url.
+ /// </summary>
+ /// <param name="text">The text with the url to parse.</param>
+ /// <param name="tvdbId">The parsed TVDb id.</param>
+ /// <returns>True if parsing was successful, false otherwise.</returns>
+ public static bool TryFindTvdbId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tvdbId)
+ => TryFindProviderId(text, "thetvdb.com/?tab=series&id=", out tvdbId);
+
+ private static bool TryFindProviderId(ReadOnlySpan<char> text, ReadOnlySpan<char> searchString, [NotNullWhen(true)] out ReadOnlySpan<char> providerId)
+ {
+ var searchPos = text.IndexOf(searchString);
+ if (searchPos == -1)
+ {
+ providerId = default;
+ return false;
+ }
+
+ text = text.Slice(searchPos + searchString.Length);
+
+ int i = 0;
+ for (; i < text.Length; i++)
+ {
+ var c = text[i];
+
+ if (!IsDigit(c))
+ {
+ break;
+ }
+ }
+
+ if (i >= 1)
+ {
+ providerId = text.Slice(0, i);
+ return true;
+ }
+
+ providerId = default;
+ return false;
+ }
+
+ private static bool IsDigit(char c)
+ {
+ return c >= '0' && c <= '9';
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
index 0844c2d79..458494bdc 100644
--- a/MediaBrowser.Common/Updates/IInstallationManager.cs
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Threading;
@@ -27,7 +25,7 @@ namespace MediaBrowser.Common.Updates
/// <param name="filterIncompatible">Filter out incompatible plugins.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
- Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
+ Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all available packages that are supported by this version.
@@ -47,7 +45,7 @@ namespace MediaBrowser.Common.Updates
IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? specificVersion = null);
/// <summary>
@@ -62,7 +60,7 @@ namespace MediaBrowser.Common.Updates
IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? minVersion = null,
Version? specificVersion = null);
diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs
index adf336313..f4f759955 100644
--- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs
+++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Model.Updates;
diff --git a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
index 46f10c84f..d37146195 100644
--- a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
+++ b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
index 4249a9a66..635e4eb3d 100644
--- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
+++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
index ecdffa2eb..a56d3c822 100644
--- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Threading.Tasks;
diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
index 6729b9115..8c9d1baf8 100644
--- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index 31dd95402..ffc274c5d 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -1,7 +1,9 @@
+#nullable disable
+
using System;
using System.Linq;
using System.Threading;
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -51,7 +53,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
- return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
if (!libraryOptions.EnableInternetProviders)
@@ -61,7 +63,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
- return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -82,7 +84,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
- return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
if (!libraryOptions.EnableInternetProviders)
@@ -92,7 +94,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
- return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
index e1f5d05a6..b2b36c040 100644
--- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
@@ -1,4 +1,5 @@
-using System;
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index b2315bda4..26a936be0 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -82,7 +84,7 @@ namespace MediaBrowser.Controller.Channels
internal static bool IsChannelVisible(BaseItem channelItem, User user)
{
- var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(""));
+ var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(string.Empty));
return channel.IsVisible(user);
}
diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
index 476992cbd..4d1e35f9e 100644
--- a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
+++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,6 +13,19 @@ namespace MediaBrowser.Controller.Channels
{
public class ChannelItemInfo : IHasProviderIds
{
+ public ChannelItemInfo()
+ {
+ MediaSources = new List<MediaSourceInfo>();
+ TrailerTypes = new List<TrailerType>();
+ Genres = new List<string>();
+ Studios = new List<string>();
+ People = new List<PersonInfo>();
+ Tags = new List<string>();
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ Artists = new List<string>();
+ AlbumArtists = new List<string>();
+ }
+
public string Name { get; set; }
public string SeriesName { get; set; }
@@ -78,18 +93,5 @@ namespace MediaBrowser.Controller.Channels
public bool IsLiveStream { get; set; }
public string Etag { get; set; }
-
- public ChannelItemInfo()
- {
- MediaSources = new List<MediaSourceInfo>();
- TrailerTypes = new List<TrailerType>();
- Genres = new List<string>();
- Studios = new List<string>();
- People = new List<PersonInfo>();
- Tags = new List<string>();
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- Artists = new List<string>();
- AlbumArtists = new List<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs
index cee7b2003..6b2077662 100644
--- a/MediaBrowser.Controller/Channels/ChannelItemResult.cs
+++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -6,13 +8,13 @@ namespace MediaBrowser.Controller.Channels
{
public class ChannelItemResult
{
- public List<ChannelItemInfo> Items { get; set; }
-
- public int? TotalRecordCount { get; set; }
-
public ChannelItemResult()
{
Items = new List<ChannelItemInfo>();
}
+
+ public List<ChannelItemInfo> Items { get; set; }
+
+ public int? TotalRecordCount { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs b/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs
new file mode 100644
index 000000000..6f0761e64
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs
@@ -0,0 +1,11 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Controller.Channels
+{
+ public class ChannelLatestMediaSearch
+ {
+ public string UserId { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
index 32469d4d7..990b025bc 100644
--- a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
+++ b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
@@ -8,9 +10,4 @@ namespace MediaBrowser.Controller.Channels
public string UserId { get; set; }
}
-
- public class ChannelLatestMediaSearch
- {
- public string UserId { get; set; }
- }
}
diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs
index 2c0eadf95..01bf8d5c8 100644
--- a/MediaBrowser.Controller/Channels/IChannel.cs
+++ b/MediaBrowser.Controller/Channels/IChannel.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs
index ddae7dbd3..49be897ef 100644
--- a/MediaBrowser.Controller/Channels/IChannelManager.cs
+++ b/MediaBrowser.Controller/Channels/IChannelManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -49,32 +51,47 @@ namespace MediaBrowser.Controller.Channels
/// Gets the channels internal.
/// </summary>
/// <param name="query">The query.</param>
+ /// <returns>The channels.</returns>
QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
/// <summary>
/// Gets the channels.
/// </summary>
/// <param name="query">The query.</param>
+ /// <returns>The channels.</returns>
QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
/// <summary>
- /// Gets the latest media.
+ /// Gets the latest channel items.
/// </summary>
+ /// <param name="query">The item query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The latest channels.</returns>
Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
/// <summary>
- /// Gets the latest media.
+ /// Gets the latest channel items.
/// </summary>
+ /// <param name="query">The item query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The latest channels.</returns>
Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken);
/// <summary>
/// Gets the channel items.
/// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The channel items.</returns>
Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
/// <summary>
- /// Gets the channel items internal.
+ /// Gets the channel items.
/// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="progress">The progress to report to.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The channel items.</returns>
Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
@@ -82,9 +99,14 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns>
+ /// <returns>The item media sources.</returns>
IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken);
+ /// <summary>
+ /// Whether the item supports media probe.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>Whether media probe should be enabled.</returns>
bool EnableMediaProbe(BaseItem item);
}
}
diff --git a/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
new file mode 100644
index 000000000..0539b9048
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
@@ -0,0 +1,12 @@
+namespace MediaBrowser.Controller.Channels
+{
+ /// <summary>
+ /// Disable media source display.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="Channel"/> can inherit this interface to disable being displayed.
+ /// </remarks>
+ public interface IDisableMediaSourceDisplay
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
index bf895a0ec..9fae43033 100644
--- a/MediaBrowser.Controller/Channels/IHasCacheKey.cs
+++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
diff --git a/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs
new file mode 100644
index 000000000..47277a8cc
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs
@@ -0,0 +1,9 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface IHasFolderAttributes
+ {
+ string[] Attributes { get; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
index 589295543..eeaa6b622 100644
--- a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
+++ b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -7,11 +5,17 @@ using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.Channels
{
+ /// <summary>
+ /// The channel requires a media info callback.
+ /// </summary>
public interface IRequiresMediaInfoCallback
{
/// <summary>
/// Gets the channel item media information.
/// </summary>
+ /// <param name="id">The channel item id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The enumerable of media source info.</returns>
Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaInfo(string id, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs
index b627ca1c2..b87943a6e 100644
--- a/MediaBrowser.Controller/Channels/ISearchableChannel.cs
+++ b/MediaBrowser.Controller/Channels/ISearchableChannel.cs
@@ -1,9 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Channels
{
@@ -17,35 +18,4 @@ namespace MediaBrowser.Controller.Channels
/// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken);
}
-
- public interface ISupportsLatestMedia
- {
- /// <summary>
- /// Gets the latest media.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
- Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken);
- }
-
- public interface ISupportsDelete
- {
- bool CanDelete(BaseItem item);
-
- Task DeleteItem(string id, CancellationToken cancellationToken);
- }
-
- public interface IDisableMediaSourceDisplay
- {
- }
-
- public interface ISupportsMediaProbe
- {
- }
-
- public interface IHasFolderAttributes
- {
- string[] Attributes { get; }
- }
}
diff --git a/MediaBrowser.Controller/Channels/ISupportsDelete.cs b/MediaBrowser.Controller/Channels/ISupportsDelete.cs
new file mode 100644
index 000000000..204054374
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ISupportsDelete.cs
@@ -0,0 +1,15 @@
+#pragma warning disable CS1591
+
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface ISupportsDelete
+ {
+ bool CanDelete(BaseItem item);
+
+ Task DeleteItem(string id, CancellationToken cancellationToken);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
new file mode 100644
index 000000000..dbba7cba2
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
@@ -0,0 +1,21 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface ISupportsLatestMedia
+ {
+ /// <summary>
+ /// Gets the latest media.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The latest media.</returns>
+ Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
new file mode 100644
index 000000000..bc7683125
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Channels
+{
+ /// <summary>
+ /// Channel supports media probe.
+ /// </summary>
+ public interface ISupportsMediaProbe
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
index 137f5d095..45cd08173 100644
--- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
+++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -28,7 +30,7 @@ namespace MediaBrowser.Controller.Channels
public List<ChannelMediaContentType> ContentTypes { get; set; }
/// <summary>
- /// Represents the maximum number of records the channel allows retrieving at a time.
+ /// Gets or sets the maximum number of records the channel allows retrieving at a time.
/// </summary>
public int? MaxPageSize { get; set; }
@@ -39,7 +41,7 @@ namespace MediaBrowser.Controller.Channels
public List<ChannelItemSortField> DefaultSortFields { get; set; }
/// <summary>
- /// Indicates if a sort ascending/descending toggle is supported or not.
+ /// Gets or sets a value indicating whether a sort ascending/descending toggle is supported or not.
/// </summary>
public bool SupportsSortOrderToggle { get; set; }
diff --git a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
index 7e9bb28ed..0d837faca 100644
--- a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
+++ b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs b/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs
new file mode 100644
index 000000000..82b3a4977
--- /dev/null
+++ b/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs
@@ -0,0 +1,24 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using MediaBrowser.Controller.Entities.Movies;
+
+namespace MediaBrowser.Controller.Collections
+{
+ public class CollectionCreatedEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the collection.
+ /// </summary>
+ /// <value>The collection.</value>
+ public BoxSet Collection { get; set; }
+
+ /// <summary>
+ /// Gets or sets the options.
+ /// </summary>
+ /// <value>The options.</value>
+ public CollectionCreationOptions Options { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
index f6037d05e..30f5f4efa 100644
--- a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
+++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -23,8 +25,8 @@ namespace MediaBrowser.Controller.Collections
public Dictionary<string, string> ProviderIds { get; set; }
- public string[] ItemIdList { get; set; }
+ public IReadOnlyList<string> ItemIdList { get; set; }
- public Guid[] UserIds { get; set; }
+ public IReadOnlyList<Guid> UserIds { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Collections/CollectionEvents.cs b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs
index ce59b4ada..8155cf3db 100644
--- a/MediaBrowser.Controller/Collections/CollectionEvents.cs
+++ b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,23 +9,14 @@ using MediaBrowser.Controller.Entities.Movies;
namespace MediaBrowser.Controller.Collections
{
- public class CollectionCreatedEventArgs : EventArgs
- {
- /// <summary>
- /// Gets or sets the collection.
- /// </summary>
- /// <value>The collection.</value>
- public BoxSet Collection { get; set; }
-
- /// <summary>
- /// Gets or sets the options.
- /// </summary>
- /// <value>The options.</value>
- public CollectionCreationOptions Options { get; set; }
- }
-
public class CollectionModifiedEventArgs : EventArgs
{
+ public CollectionModifiedEventArgs(BoxSet collection, IReadOnlyCollection<BaseItem> itemsChanged)
+ {
+ Collection = collection;
+ ItemsChanged = itemsChanged;
+ }
+
/// <summary>
/// Gets or sets the collection.
/// </summary>
@@ -34,6 +27,6 @@ namespace MediaBrowser.Controller.Collections
/// Gets or sets the items changed.
/// </summary>
/// <value>The items changed.</value>
- public List<BaseItem> ItemsChanged { get; set; }
+ public IReadOnlyCollection<BaseItem> ItemsChanged { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index a6991e2ea..46bc37e7f 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
index 43ad04dba..44e2c45dd 100644
--- a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
+++ b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration;
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index 8f0872dba..8096be1bd 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -18,7 +20,6 @@ namespace MediaBrowser.Controller.Devices
/// </summary>
/// <param name="reportedId">The reported identifier.</param>
/// <param name="capabilities">The capabilities.</param>
- /// <returns>Task.</returns>
void SaveCapabilities(string reportedId, ClientCapabilities capabilities);
/// <summary>
@@ -45,6 +46,9 @@ namespace MediaBrowser.Controller.Devices
/// <summary>
/// Determines whether this instance [can access device] the specified user identifier.
/// </summary>
+ /// <param name="user">The user to test.</param>
+ /// <param name="deviceId">The device id to test.</param>
+ /// <returns>Whether the user can access the device.</returns>
bool CanAccessDevice(User user, string deviceId);
void UpdateDeviceOptions(string deviceId, DeviceOptions options);
diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
index dc2d5a356..b51dc255c 100644
--- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs
+++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index 770c6dc2d..4e640d421 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#nullable enable
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index 935a79031..c7f61a90b 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#nullable enable
using System;
using System.Collections.Generic;
@@ -61,7 +60,7 @@ namespace MediaBrowser.Controller.Drawing
string GetImageCacheTag(BaseItem item, ChapterInfo info);
- string GetImageCacheTag(User user);
+ string? GetImageCacheTag(User user);
/// <summary>
/// Processes the image.
diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
index fe0465d0d..e9c88ffb5 100644
--- a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
@@ -1,3 +1,7 @@
+#nullable disable
+
+using System.Collections.Generic;
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Drawing
@@ -8,7 +12,7 @@ namespace MediaBrowser.Controller.Drawing
/// Gets or sets the input paths.
/// </summary>
/// <value>The input paths.</value>
- public string[] InputPaths { get; set; }
+ public IReadOnlyList<string> InputPaths { get; set; }
/// <summary>
/// Gets or sets the output path.
diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs
index 87c28d577..9ef92bc98 100644
--- a/MediaBrowser.Controller/Drawing/ImageHelper.cs
+++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs
@@ -1,75 +1,17 @@
#pragma warning disable CS1591
-using System;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Drawing
{
public static class ImageHelper
{
- public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions? originalImageSize)
+ public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions originalImageSize)
{
- if (originalImageSize.HasValue)
- {
- // Determine the output size based on incoming parameters
- var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
-
- return newSize;
- }
-
- return GetSizeEstimate(options);
- }
-
- private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options)
- {
- if (options.Width.HasValue && options.Height.HasValue)
- {
- return new ImageDimensions(options.Width.Value, options.Height.Value);
- }
-
- double aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item);
-
- int? width = options.Width ?? options.MaxWidth;
-
- if (width.HasValue)
- {
- int heightValue = Convert.ToInt32((double)width.Value / aspect);
- return new ImageDimensions(width.Value, heightValue);
- }
-
- var height = options.Height ?? options.MaxHeight ?? 200;
- int widthValue = Convert.ToInt32(aspect * height);
- return new ImageDimensions(widthValue, height);
- }
-
- private static double GetEstimatedAspectRatio(ImageType type, BaseItem item)
- {
- switch (type)
- {
- case ImageType.Art:
- case ImageType.Backdrop:
- case ImageType.Chapter:
- case ImageType.Screenshot:
- case ImageType.Thumb:
- return 1.78;
- case ImageType.Banner:
- return 5.4;
- case ImageType.Box:
- case ImageType.BoxRear:
- case ImageType.Disc:
- case ImageType.Menu:
- case ImageType.Profile:
- return 1;
- case ImageType.Logo:
- return 2.58;
- case ImageType.Primary:
- double defaultPrimaryImageAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
- return defaultPrimaryImageAspectRatio > 0 ? defaultPrimaryImageAspectRatio : 2.0 / 3;
- default:
- return 1;
- }
+ // Determine the output size based on incoming parameters
+ var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
+ newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight);
+ return newSize;
}
}
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
index 22105b7d7..11e663301 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -24,8 +26,6 @@ namespace MediaBrowser.Controller.Drawing
public int ImageIndex { get; set; }
- public bool CropWhiteSpace { get; set; }
-
public int? Width { get; set; }
public int? Height { get; set; }
@@ -34,6 +34,10 @@ namespace MediaBrowser.Controller.Drawing
public int? MaxHeight { get; set; }
+ public int? FillWidth { get; set; }
+
+ public int? FillHeight { get; set; }
+
public int Quality { get; set; }
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
@@ -95,6 +99,11 @@ namespace MediaBrowser.Controller.Drawing
return false;
}
+ if (sizeValue.Width > FillWidth || sizeValue.Height > FillHeight)
+ {
+ return false;
+ }
+
return true;
}
@@ -106,7 +115,6 @@ namespace MediaBrowser.Controller.Drawing
PercentPlayed.Equals(0) &&
!UnplayedCount.HasValue &&
!Blur.HasValue &&
- !CropWhiteSpace &&
string.IsNullOrEmpty(BackgroundColor) &&
string.IsNullOrEmpty(ForegroundLayer);
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
index d3a2b4dbf..b036425ab 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs
index 46f58ec15..5ee781ffa 100644
--- a/MediaBrowser.Controller/Drawing/ImageStream.cs
+++ b/MediaBrowser.Controller/Drawing/ImageStream.cs
@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Drawing
/// Gets or sets the stream.
/// </summary>
/// <value>The stream.</value>
- public Stream Stream { get; set; }
+ public Stream? Stream { get; set; }
/// <summary>
/// Gets or sets the format.
@@ -22,9 +22,15 @@ namespace MediaBrowser.Controller.Drawing
public void Dispose()
{
- if (Stream != null)
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
{
- Stream.Dispose();
+ Stream?.Dispose();
}
}
}
diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
index 356783750..ecc833154 100644
--- a/MediaBrowser.Controller/Dto/DtoOptions.cs
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -16,37 +18,17 @@ namespace MediaBrowser.Controller.Dto
ItemFields.RefreshState
};
- public IReadOnlyList<ItemFields> Fields { get; set; }
-
- public IReadOnlyList<ImageType> ImageTypes { get; set; }
-
- public int ImageTypeLimit { get; set; }
-
- public bool EnableImages { get; set; }
-
- public bool AddProgramRecordingInfo { get; set; }
-
- public bool EnableUserData { get; set; }
+ private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
- public bool AddCurrentProgram { get; set; }
+ private static readonly ItemFields[] AllItemFields = Enum.GetValues<ItemFields>()
+ .Except(DefaultExcludedFields)
+ .ToArray();
public DtoOptions()
: this(true)
{
}
- private static readonly ImageType[] AllImageTypes = Enum.GetNames(typeof(ImageType))
- .Select(i => (ImageType)Enum.Parse(typeof(ImageType), i, true))
- .ToArray();
-
- private static readonly ItemFields[] AllItemFields = Enum.GetNames(typeof(ItemFields))
- .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
- .Except(DefaultExcludedFields)
- .ToArray();
-
- public bool ContainsField(ItemFields field)
- => Fields.Contains(field);
-
public DtoOptions(bool allFields)
{
ImageTypeLimit = int.MaxValue;
@@ -58,6 +40,23 @@ namespace MediaBrowser.Controller.Dto
ImageTypes = AllImageTypes;
}
+ public IReadOnlyList<ItemFields> Fields { get; set; }
+
+ public IReadOnlyList<ImageType> ImageTypes { get; set; }
+
+ public int ImageTypeLimit { get; set; }
+
+ public bool EnableImages { get; set; }
+
+ public bool AddProgramRecordingInfo { get; set; }
+
+ public bool EnableUserData { get; set; }
+
+ public bool AddCurrentProgram { get; set; }
+
+ public bool ContainsField(ItemFields field)
+ => Fields.Contains(field);
+
public int GetImageLimit(ImageType type)
{
if (EnableImages && ImageTypes.Contains(type))
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index 988557f42..61d796235 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
@@ -34,11 +36,17 @@ namespace MediaBrowser.Controller.Dto
/// <param name="options">The options.</param>
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
+ /// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns>
IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
/// <summary>
/// Gets the item by name dto.
/// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="options">The dto options.</param>
+ /// <param name="taggedItems">The list of tagged items.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>The item dto.</returns>
BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null);
}
}
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index 6ebea5f44..fe1bc62ab 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -20,6 +22,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class AggregateFolder : Folder
{
+ private bool _requiresRefresh;
+
public AggregateFolder()
{
PhysicalLocationsList = Array.Empty<string>();
@@ -83,7 +87,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- private bool _requiresRefresh;
public override bool RequiresRefresh()
{
var changed = base.RequiresRefresh() || _requiresRefresh;
@@ -103,11 +106,11 @@ namespace MediaBrowser.Controller.Entities
return changed;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
ClearCache();
- var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh;
+ var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh;
_requiresRefresh = false;
return changed;
}
@@ -120,8 +123,7 @@ namespace MediaBrowser.Controller.Entities
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
{
- FileInfo = FileSystem.GetDirectoryInfo(path),
- Path = path
+ FileInfo = FileSystem.GetDirectoryInfo(path)
};
// Gather child folder and files
@@ -153,11 +155,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
@@ -183,7 +185,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
+ /// <exception cref="ArgumentNullException">The id is empty.</exception>
public BaseItem FindVirtualChild(Guid id)
{
if (id.Equals(Guid.Empty))
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index 8220464b3..576ab67a2 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -1,7 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
@@ -80,19 +83,19 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "")
- + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty)
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
}
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
- var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty;
+ var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : string.Empty;
if (ParentIndexNumber.HasValue)
{
- songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey;
+ songKey = ParentIndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) + "-" + songKey;
}
songKey += Name;
diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
index f6d3cd6cc..1625c748a 100644
--- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
@@ -1,6 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Entities.Audio
{
@@ -23,15 +27,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public static IEnumerable<string> GetAllArtists<T>(this T item)
where T : IHasArtist, IHasAlbumArtist
{
- foreach (var i in item.AlbumArtists)
- {
- yield return i;
- }
-
- foreach (var i in item.Artists)
- {
- yield return i;
- }
+ return item.AlbumArtists.Concat(item.Artists).DistinctNames();
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
index ac4dd1688..db60c3071 100644
--- a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
+++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Entities.Audio
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 9a33ad9d7..610bce4f5 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 8a9bb12c7..c0cd81110 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -92,7 +94,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return base.IsSaveLocalMetadataEnabled();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (IsAccessedByName)
{
@@ -100,7 +102,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return Task.CompletedTask;
}
- return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
+ return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken);
}
public override List<string> GetUserDataKeys()
@@ -112,7 +114,7 @@ namespace MediaBrowser.Controller.Entities.Audio
}
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
@@ -206,9 +208,9 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (IsAccessedByName)
{
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
index f0c076108..a682a2e58 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -36,7 +38,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public override bool IsDisplayedAsFolder => true;
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
@@ -104,9 +106,9 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs
index f4bd851e1..405284622 100644
--- a/MediaBrowser.Controller/Entities/AudioBook.cs
+++ b/MediaBrowser.Controller/Entities/AudioBook.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 7598b29e6..6137ddbf7 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -90,7 +92,8 @@ namespace MediaBrowser.Controller.Entities
public const string ShortsFolderName = "shorts";
public const string FeaturettesFolderName = "featurettes";
- public static readonly string[] AllExtrasTypesFolderNames = {
+ public static readonly string[] AllExtrasTypesFolderNames =
+ {
ExtrasFolderName,
BehindTheScenesFolderName,
DeletedScenesFolderName,
@@ -106,15 +109,10 @@ namespace MediaBrowser.Controller.Entities
{
get
{
- if (_themeSongIds == null)
- {
- _themeSongIds = GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong)
- .Select(song => song.Id)
- .ToArray();
- }
-
- return _themeSongIds;
+ return _themeSongIds ??= GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong)
+ .Select(song => song.Id)
+ .ToArray();
}
private set
@@ -128,15 +126,10 @@ namespace MediaBrowser.Controller.Entities
{
get
{
- if (_themeVideoIds == null)
- {
- _themeVideoIds = GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo)
- .Select(song => song.Id)
- .ToArray();
- }
-
- return _themeVideoIds;
+ return _themeVideoIds ??= GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo)
+ .Select(song => song.Id)
+ .ToArray();
}
private set
@@ -185,7 +178,7 @@ namespace MediaBrowser.Controller.Entities
public virtual bool AlwaysScanInternalMetadataPath => false;
/// <summary>
- /// Gets a value indicating whether this instance is in mixed folder.
+ /// Gets or sets a value indicating whether this instance is in mixed folder.
/// </summary>
/// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
[JsonIgnore]
@@ -252,7 +245,7 @@ namespace MediaBrowser.Controller.Entities
public ProgramAudio? Audio { get; set; }
/// <summary>
- /// Return the id that should be used to key display prefs for this item.
+ /// Gets the id that should be used to key display prefs for this item.
/// Default is based on the type for everything except actual generic folders.
/// </summary>
/// <value>The display prefs id.</value>
@@ -288,7 +281,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
[JsonIgnore]
@@ -313,8 +306,11 @@ namespace MediaBrowser.Controller.Entities
public string ServiceName { get; set; }
/// <summary>
- /// If this content came from an external service, the id of the content on that service.
+ /// Gets or sets the external id.
/// </summary>
+ /// <remarks>
+ /// If this content came from an external service, the id of the content on that service.
+ /// </remarks>
[JsonIgnore]
public string ExternalId { get; set; }
@@ -338,7 +334,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Gets or sets the type of the location.
+ /// Gets the type of the location.
/// </summary>
/// <value>The type of the location.</value>
[JsonIgnore]
@@ -347,9 +343,9 @@ namespace MediaBrowser.Controller.Entities
get
{
// if (IsOffline)
- //{
+ // {
// return LocationType.Offline;
- //}
+ // }
var path = Path;
if (string.IsNullOrEmpty(path))
@@ -457,8 +453,11 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// This is just a helper for convenience.
+ /// Gets the primary image path.
/// </summary>
+ /// <remarks>
+ /// This is just a helper for convenience.
+ /// </remarks>
/// <value>The primary image path.</value>
[JsonIgnore]
public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
@@ -549,7 +548,7 @@ namespace MediaBrowser.Controller.Entities
public DateTime DateLastRefreshed { get; set; }
/// <summary>
- /// The logger.
+ /// Gets or sets the logger.
/// </summary>
public static ILogger<BaseItem> Logger { get; set; }
@@ -621,7 +620,11 @@ namespace MediaBrowser.Controller.Entities
public string ForcedSortName
{
get => _forcedSortName;
- set { _forcedSortName = value; _sortName = null; }
+ set
+ {
+ _forcedSortName = value;
+ _sortName = null;
+ }
}
private string _sortName;
@@ -629,7 +632,7 @@ namespace MediaBrowser.Controller.Entities
private Guid[] _themeVideoIds;
/// <summary>
- /// Gets the name of the sort.
+ /// Gets or sets the name of the sort.
/// </summary>
/// <value>The name of the sort.</value>
[JsonIgnore]
@@ -667,14 +670,12 @@ namespace MediaBrowser.Controller.Entities
{
if (SourceType == SourceType.Channel)
{
- return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
+ return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
}
ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
- basePath = System.IO.Path.Combine(basePath, "library");
-
- return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString);
+ return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString);
}
/// <summary>
@@ -856,7 +857,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
+ /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be first aired.
/// </summary>
/// <value>The premiere date.</value>
[JsonIgnore]
@@ -953,7 +954,7 @@ namespace MediaBrowser.Controller.Entities
public int? ProductionYear { get; set; }
/// <summary>
- /// If the item is part of a series, this is it's number in the series.
+ /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
/// This could be episode number, album track number, etc.
/// </summary>
/// <value>The index number.</value>
@@ -961,7 +962,7 @@ namespace MediaBrowser.Controller.Entities
public int? IndexNumber { get; set; }
/// <summary>
- /// For an episode this could be the season number, or for a song this could be the disc number.
+ /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this could be the disc number.
/// </summary>
/// <value>The parent index number.</value>
[JsonIgnore]
@@ -1025,9 +1026,9 @@ namespace MediaBrowser.Controller.Entities
}
// if (!user.IsParentalScheduleAllowed())
- //{
+ // {
// return PlayAccess.None;
- //}
+ // }
return PlayAccess.Full;
}
@@ -1243,7 +1244,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- return string.Join("/", terms.ToArray());
+ return string.Join('/', terms.ToArray());
}
/// <summary>
@@ -1259,7 +1260,7 @@ namespace MediaBrowser.Controller.Entities
// Support plex/xbmc convention
files.AddRange(fileSystemChildren
- .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
+ .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
.OfType<Audio.Audio>()
@@ -1320,14 +1321,16 @@ namespace MediaBrowser.Controller.Entities
{
var extras = new List<Video>();
- var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray();
+ var libraryOptions = new LibraryOptions();
+ var folders = fileSystemChildren.Where(i => i.IsDirectory).ToList();
foreach (var extraFolderName in AllExtrasTypesFolderNames)
{
var files = folders
.Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => FileSystem.GetFiles(i.FullName));
- extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
+ // Re-using the same instance of LibraryOptions since it looks like it's never being altered.
+ extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, libraryOptions)
.OfType<Video>()
.Select(item =>
{
@@ -1338,7 +1341,7 @@ namespace MediaBrowser.Controller.Entities
}
// Use some hackery to get the extra type based on foldername
- item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty), true, out ExtraType extraType)
+ item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty, StringComparison.Ordinal), true, out ExtraType extraType)
? extraType
: Model.Entities.ExtraType.Unknown;
@@ -1428,10 +1431,10 @@ namespace MediaBrowser.Controller.Entities
/// Refreshes owned items such as trailers, theme videos, special features, etc.
/// Returns true or false indicating if changes were found.
/// </summary>
- /// <param name="options"></param>
- /// <param name="fileSystemChildren"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="options">The metadata refresh options.</param>
+ /// <param name="fileSystemChildren">The list of filesystem children.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var themeSongsChanged = false;
@@ -1773,7 +1776,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="user">The user.</param>
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
+ /// <exception cref="ArgumentNullException">If user is null.</exception>
public bool IsParentalAllowed(User user)
{
if (user == null)
@@ -1918,7 +1921,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="user">The user.</param>
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
public virtual bool IsVisible(User user)
{
if (user == null)
@@ -2216,7 +2219,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="type">The type.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
+ /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
public bool HasImage(ImageType type, int imageIndex)
{
return GetImageInfo(type, imageIndex) != null;
@@ -2324,7 +2327,7 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i.IsLocalFile)
.Select(i => System.IO.Path.GetDirectoryName(i.Path))
.Distinct(StringComparer.OrdinalIgnoreCase)
- .SelectMany(i => directoryService.GetFilePaths(i))
+ .SelectMany(path => directoryService.GetFilePaths(path))
.ToList();
var deletedImages = ImageInfos
@@ -2345,9 +2348,8 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns>System.String.</returns>
- /// <exception cref="InvalidOperationException">
- /// </exception>
- /// <exception cref="ArgumentNullException">item</exception>
+ /// <exception cref="InvalidOperationException"> </exception>
+ /// <exception cref="ArgumentNullException">Item is null.</exception>
public string GetImagePath(ImageType imageType, int imageIndex)
=> GetImageInfo(imageType, imageIndex)?.Path;
@@ -2434,7 +2436,15 @@ namespace MediaBrowser.Controller.Entities
throw new ArgumentException("No image info for chapter images");
}
- return ImageInfos.Where(i => i.Type == imageType);
+ // Yield return is more performant than LINQ Where on an Array
+ for (var i = 0; i < ImageInfos.Length; i++)
+ {
+ var imageInfo = ImageInfos[i];
+ if (imageInfo.Type == imageType)
+ {
+ yield return imageInfo;
+ }
+ }
}
/// <summary>
@@ -2443,7 +2453,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="images">The images.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
- /// <exception cref="ArgumentException">Cannot call AddImages with chapter images</exception>
+ /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
{
if (imageType == ImageType.Chapter)
@@ -2466,7 +2476,7 @@ namespace MediaBrowser.Controller.Entities
}
var existing = existingImages
- .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
+ .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
@@ -2497,8 +2507,7 @@ namespace MediaBrowser.Controller.Entities
var newImagePaths = images.Select(i => i.FullName).ToList();
var deleted = existingImages
- .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path))
- .ToList();
+ .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path));
if (deleted.Count > 0)
{
@@ -2527,10 +2536,11 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the file system path to delete when the item is to be deleted.
/// </summary>
- /// <returns></returns>
+ /// <returns>The metadata for the deleted paths.</returns>
public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = Path,
@@ -2637,6 +2647,7 @@ namespace MediaBrowser.Controller.Entities
MetadataCountryCode = GetPreferredMetadataCountryCode(),
MetadataLanguage = GetPreferredMetadataLanguage(),
Name = GetNameForMetadataLookup(),
+ OriginalTitle = OriginalTitle,
ProviderIds = ProviderIds,
IndexNumber = IndexNumber,
ParentIndexNumber = ParentIndexNumber,
@@ -2653,7 +2664,9 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is called before any metadata refresh and returns true if changes were made.
/// </summary>
- public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
+ /// <returns>true if the item has change, else false.</returns>
+ public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
_sortName = null;
@@ -2777,11 +2790,11 @@ namespace MediaBrowser.Controller.Entities
// var parentId = Id;
// if (!video.IsOwnedItem || video.ParentId != parentId)
- //{
+ // {
// video.IsOwnedItem = true;
// video.ParentId = parentId;
// newOptions.ForceSave = true;
- //}
+ // }
if (video == null)
{
@@ -2795,7 +2808,7 @@ namespace MediaBrowser.Controller.Entities
{
var list = GetEtagValues(user);
- return string.Join("|", list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
protected virtual List<string> GetEtagValues(User user)
@@ -2888,7 +2901,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Updates the official rating based on content and returns true or false indicating if it changed.
/// </summary>
- /// <returns></returns>
+ /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
public bool UpdateRatingToItems(IList<BaseItem> children)
{
var currentOfficialRating = OfficialRating;
@@ -2904,7 +2917,9 @@ namespace MediaBrowser.Controller.Entities
OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
- return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty,
+ return !string.Equals(
+ currentOfficialRating ?? string.Empty,
+ OfficialRating ?? string.Empty,
StringComparison.OrdinalIgnoreCase);
}
@@ -3001,7 +3016,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- public bool Equals(BaseItem item) => Object.Equals(Id, item?.Id);
+ public bool Equals(BaseItem other) => object.Equals(Id, other?.Id);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(Id);
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index c65477d39..89ad392a4 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using System.Linq;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -64,9 +65,19 @@ namespace MediaBrowser.Controller.Entities
/// <param name="source">The source object.</param>
/// <param name="dest">The destination object.</param>
public static void DeepCopy<T, TU>(this T source, TU dest)
- where T : BaseItem
- where TU : BaseItem
+ where T : BaseItem
+ where TU : BaseItem
{
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (dest == null)
+ {
+ throw new ArgumentNullException(nameof(dest));
+ }
+
var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList();
foreach (var sourceProp in typeof(T).GetProperties())
@@ -99,8 +110,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="source">The source object.</param>
public static TU DeepCopy<T, TU>(this T source)
- where T : BaseItem
- where TU : BaseItem, new()
+ where T : BaseItem
+ where TU : BaseItem, new()
{
var dest = new TU();
source.DeepCopy(dest);
diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
index ef5a5a734..1bd25042f 100644
--- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs
+++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs
index 55945283c..d75beb06d 100644
--- a/MediaBrowser.Controller/Entities/Book.cs
+++ b/MediaBrowser.Controller/Entities/Book.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,6 +12,11 @@ namespace MediaBrowser.Controller.Entities
{
public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries
{
+ public Book()
+ {
+ this.RunTimeTicks = TimeSpan.TicksPerSecond;
+ }
+
[JsonIgnore]
public override string MediaType => Model.Entities.MediaType.Book;
@@ -26,11 +33,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public Guid SeriesId { get; set; }
- public Book()
- {
- this.RunTimeTicks = TimeSpan.TicksPerSecond;
- }
-
public string FindSeriesSortName()
{
return SeriesName;
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 65fd1654c..4a721ca44 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -26,31 +28,46 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class CollectionFolder : Folder, ICollectionFolder
{
- private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
- public static IXmlSerializer XmlSerializer { get; set; }
-
- public static IServerApplicationHost ApplicationHost { get; set; }
+ private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
+ private bool _requiresRefresh;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CollectionFolder"/> class.
+ /// </summary>
public CollectionFolder()
{
PhysicalLocationsList = Array.Empty<string>();
PhysicalFolderIds = Array.Empty<Guid>();
}
+ public static IXmlSerializer XmlSerializer { get; set; }
+
+ public static IServerApplicationHost ApplicationHost { get; set; }
+
[JsonIgnore]
public override bool SupportsPlayedStatus => false;
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
+ public string CollectionType { get; set; }
+
+ /// <summary>
+ /// Gets the item's children.
+ /// </summary>
+ /// <remarks>
+ /// Our children are actually just references to the ones in the physical root...
+ /// </remarks>
+ /// <value>The actual children.</value>
+ [JsonIgnore]
+ public override IEnumerable<BaseItem> Children => GetActualChildren();
+
public override bool CanDelete()
{
return false;
}
- public string CollectionType { get; set; }
-
- private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>();
public LibraryOptions GetLibraryOptions()
{
return GetLibraryOptions(Path);
@@ -61,7 +78,6 @@ namespace MediaBrowser.Controller.Entities
try
{
var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions;
-
if (result == null)
{
return new LibraryOptions();
@@ -105,12 +121,12 @@ namespace MediaBrowser.Controller.Entities
public static LibraryOptions GetLibraryOptions(string path)
{
- lock (LibraryOptions)
+ lock (_libraryOptions)
{
- if (!LibraryOptions.TryGetValue(path, out var options))
+ if (!_libraryOptions.TryGetValue(path, out var options))
{
options = LoadLibraryOptions(path);
- LibraryOptions[path] = options;
+ _libraryOptions[path] = options;
}
return options;
@@ -119,9 +135,9 @@ namespace MediaBrowser.Controller.Entities
public static void SaveLibraryOptions(string path, LibraryOptions options)
{
- lock (LibraryOptions)
+ lock (_libraryOptions)
{
- LibraryOptions[path] = options;
+ _libraryOptions[path] = options;
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos)
@@ -138,15 +154,18 @@ namespace MediaBrowser.Controller.Entities
public static void OnCollectionFolderChange()
{
- lock (LibraryOptions)
+ lock (_libraryOptions)
{
- LibraryOptions.Clear();
+ _libraryOptions.Clear();
}
}
/// <summary>
- /// Allow different display preferences for each collection folder.
+ /// Gets the display preferences id.
/// </summary>
+ /// <remarks>
+ /// Allow different display preferences for each collection folder.
+ /// </remarks>
/// <value>The display prefs id.</value>
[JsonIgnore]
public override Guid DisplayPreferencesId => Id;
@@ -154,21 +173,20 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override string[] PhysicalLocations => PhysicalLocationsList;
+ public string[] PhysicalLocationsList { get; set; }
+
+ public Guid[] PhysicalFolderIds { get; set; }
+
public override bool IsSaveLocalMetadataEnabled()
{
return true;
}
- public string[] PhysicalLocationsList { get; set; }
-
- public Guid[] PhysicalFolderIds { get; set; }
-
protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
{
return CreateResolveArgs(directoryService, true).FileSystemChildren;
}
- private bool _requiresRefresh;
public override bool RequiresRefresh()
{
var changed = base.RequiresRefresh() || _requiresRefresh;
@@ -200,9 +218,9 @@ namespace MediaBrowser.Controller.Entities
return changed;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh;
+ var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh;
_requiresRefresh = false;
return changed;
}
@@ -271,7 +289,6 @@ namespace MediaBrowser.Controller.Entities
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
{
FileInfo = FileSystem.GetDirectoryInfo(path),
- Path = path,
Parent = GetParent() as Folder,
CollectionType = CollectionType
};
@@ -298,27 +315,20 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
- /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+ /// ***Currently does not contain logic to maintain items that are unavailable in the file system***.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
- /// <summary>
- /// Our children are actually just references to the ones in the physical root...
- /// </summary>
- /// <value>The actual children.</value>
- [JsonIgnore]
- public override IEnumerable<BaseItem> Children => GetActualChildren();
-
public IEnumerable<BaseItem> GetActualChildren()
{
return GetPhysicalFolders(true).SelectMany(c => c.Children);
@@ -355,9 +365,7 @@ namespace MediaBrowser.Controller.Entities
if (result.Count == 0)
{
- var folder = LibraryManager.FindByPath(path, true) as Folder;
-
- if (folder != null)
+ if (LibraryManager.FindByPath(path, true) is Folder folder)
{
result.Add(folder);
}
diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs
index 3a34c668c..244cc00be 100644
--- a/MediaBrowser.Controller/Entities/Extensions.cs
+++ b/MediaBrowser.Controller/Entities/Extensions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Linq;
using MediaBrowser.Common.Extensions;
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index cac5026f7..541747422 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1,8 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
@@ -36,6 +37,11 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Folder : BaseItem
{
+ public Folder()
+ {
+ LinkedChildren = Array.Empty<LinkedChild>();
+ }
+
public static IUserViewManager UserViewManager { get; set; }
/// <summary>
@@ -49,11 +55,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public DateTime? DateLastMediaAdded { get; set; }
- public Folder()
- {
- LinkedChildren = Array.Empty<LinkedChild>();
- }
-
[JsonIgnore]
public override bool SupportsThemeMedia => true;
@@ -85,6 +86,85 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual bool SupportsDateLastMediaAdded => false;
+ [JsonIgnore]
+ public override string FileNameWithoutExtension
+ {
+ get
+ {
+ if (IsFileProtocol)
+ {
+ return System.IO.Path.GetFileName(Path);
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the actual children.
+ /// </summary>
+ /// <value>The actual children.</value>
+ [JsonIgnore]
+ public virtual IEnumerable<BaseItem> Children => LoadChildren();
+
+ /// <summary>
+ /// Gets thread-safe access to all recursive children of this folder - without regard to user.
+ /// </summary>
+ /// <value>The recursive children.</value>
+ [JsonIgnore]
+ public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
+
+ [JsonIgnore]
+ protected virtual bool SupportsShortcutChildren => false;
+
+ protected virtual bool FilterLinkedChildrenPerUser => false;
+
+ [JsonIgnore]
+ protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
+
+ [JsonIgnore]
+ public virtual bool SupportsUserDataFromChildren
+ {
+ get
+ {
+ // These are just far too slow.
+ if (this is ICollectionFolder)
+ {
+ return false;
+ }
+
+ if (this is UserView)
+ {
+ return false;
+ }
+
+ if (this is UserRootFolder)
+ {
+ return false;
+ }
+
+ if (this is Channel)
+ {
+ return false;
+ }
+
+ if (SourceType != SourceType.Library)
+ {
+ return false;
+ }
+
+ if (this is IItemByName)
+ {
+ if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
public override bool CanDelete()
{
if (IsRoot)
@@ -107,20 +187,6 @@ namespace MediaBrowser.Controller.Entities
return baseResult;
}
- [JsonIgnore]
- public override string FileNameWithoutExtension
- {
- get
- {
- if (IsFileProtocol)
- {
- return System.IO.Path.GetFileName(Path);
- }
-
- return null;
- }
- }
-
protected override bool IsAllowTagFilterEnforced()
{
if (this is ICollectionFolder)
@@ -136,16 +202,12 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- [JsonIgnore]
- protected virtual bool SupportsShortcutChildren => false;
-
/// <summary>
/// Adds the child.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="InvalidOperationException">Unable to add + item.Name</exception>
+ /// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception>
public void AddChild(BaseItem item, CancellationToken cancellationToken)
{
item.SetParent(this);
@@ -168,20 +230,6 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.CreateItem(item, this);
}
- /// <summary>
- /// Gets the actual children.
- /// </summary>
- /// <value>The actual children.</value>
- [JsonIgnore]
- public virtual IEnumerable<BaseItem> Children => LoadChildren();
-
- /// <summary>
- /// thread-safe access to all recursive children of this folder - without regard to user.
- /// </summary>
- /// <value>The recursive children.</value>
- [JsonIgnore]
- public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
-
public override bool IsVisible(User user)
{
if (this is ICollectionFolder && !(this is BasePluginFolder))
@@ -225,20 +273,20 @@ namespace MediaBrowser.Controller.Entities
public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
{
- return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem)));
+ return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken: cancellationToken);
}
/// <summary>
/// Validates that the children of the folder still exist.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="metadataRefreshOptions">The metadata refresh options.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true)
+ public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, CancellationToken cancellationToken = default)
{
- return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
+ return ValidateChildrenInternal(progress, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
}
private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
@@ -278,13 +326,13 @@ namespace MediaBrowser.Controller.Entities
/// Validates the children internal.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (recursive)
{
@@ -293,7 +341,7 @@ namespace MediaBrowser.Controller.Entities
try
{
- await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false);
+ await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
}
finally
{
@@ -304,7 +352,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -526,7 +574,7 @@ namespace MediaBrowser.Controller.Entities
private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
{
return RunTasks(
- (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService),
+ (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, null, directoryService, cancellationToken),
children,
progress,
cancellationToken);
@@ -939,7 +987,13 @@ namespace MediaBrowser.Controller.Entities
}
else
{
- items = GetChildren(user, true).Where(filter);
+ // need to pass this param to the children.
+ var childQuery = new InternalItemsQuery
+ {
+ DisplayAlbumFolders = query.DisplayAlbumFolders
+ };
+
+ items = GetChildren(user, true, childQuery).Where(filter);
}
return PostFilterAndSort(items, query, true);
@@ -964,7 +1018,7 @@ namespace MediaBrowser.Controller.Entities
if (!string.IsNullOrEmpty(query.NameStartsWith))
{
- items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase));
+ items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.CurrentCultureIgnoreCase));
}
if (!string.IsNullOrEmpty(query.NameLessThan))
@@ -1275,10 +1329,23 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the children to list.
/// </summary>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query)
{
- foreach (var child in GetEligibleChildrenForRecursiveChildren(user))
+ // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
+ IEnumerable<BaseItem> children = null;
+ if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
+ {
+ children = Children;
+ query = null;
+ }
+
+ // If there are not sub-folders, proceed as normal.
+ if (children == null)
+ {
+ children = GetEligibleChildrenForRecursiveChildren(user);
+ }
+
+ foreach (var child in children)
{
bool? isVisibleToUser = null;
@@ -1427,16 +1494,19 @@ namespace MediaBrowser.Controller.Entities
return list;
}
- protected virtual bool FilterLinkedChildrenPerUser => false;
-
public bool ContainsLinkedChildByItemId(Guid itemId)
{
var linkedChildren = LinkedChildren;
foreach (var i in linkedChildren)
{
- if (i.ItemId.HasValue && i.ItemId.Value == itemId)
+ if (i.ItemId.HasValue)
{
- return true;
+ if (i.ItemId.Value == itemId)
+ {
+ return true;
+ }
+
+ continue;
}
var child = GetLinkedChild(i);
@@ -1524,9 +1594,6 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i.Item2 != null);
}
- [JsonIgnore]
- protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
-
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var changesFound = false;
@@ -1547,7 +1614,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Refreshes the linked children.
/// </summary>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
+ /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
{
if (SupportsShortcutChildren)
@@ -1690,51 +1758,6 @@ namespace MediaBrowser.Controller.Entities
return !IsPlayed(user);
}
- [JsonIgnore]
- public virtual bool SupportsUserDataFromChildren
- {
- get
- {
- // These are just far too slow.
- if (this is ICollectionFolder)
- {
- return false;
- }
-
- if (this is UserView)
- {
- return false;
- }
-
- if (this is UserRootFolder)
- {
- return false;
- }
-
- if (this is Channel)
- {
- return false;
- }
-
- if (SourceType != SourceType.Library)
- {
- return false;
- }
-
- var iItemByName = this as IItemByName;
- if (iItemByName != null)
- {
- var hasDualAccess = this as IHasDualAccess;
- if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
- {
- return false;
- }
- }
-
- return true;
- }
- }
-
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
{
if (!SupportsUserDataFromChildren)
@@ -1764,20 +1787,15 @@ namespace MediaBrowser.Controller.Entities
{
EnableImages = false
}
- });
-
- double unplayedCount = unplayedQueryResult.TotalRecordCount;
+ }).TotalRecordCount;
- dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount;
+ dto.UnplayedItemCount = unplayedQueryResult;
- if (itemDto != null && itemDto.RecursiveItemCount.HasValue)
+ if (itemDto?.RecursiveItemCount > 0)
{
- if (itemDto.RecursiveItemCount.Value > 0)
- {
- var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100;
- dto.PlayedPercentage = 100 - unplayedPercentage;
- dto.Played = dto.PlayedPercentage.Value >= 100;
- }
+ var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100;
+ dto.PlayedPercentage = 100 - unplayedPercentage;
+ dto.Played = dto.PlayedPercentage.Value >= 100;
}
else
{
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index 74a170204..b80a5be3b 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -14,6 +16,23 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Genre : BaseItem, IItemByName
{
+ /// <summary>
+ /// Gets the folder containing the item.
+ /// If the item is a folder, it returns the folder itself.
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [JsonIgnore]
+ public override string ContainingFolderPath => Path;
+
+ [JsonIgnore]
+ public override bool IsDisplayedAsFolder => true;
+
+ [JsonIgnore]
+ public override bool SupportsAncestors => false;
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
@@ -32,20 +51,6 @@ namespace MediaBrowser.Controller.Entities
return 1;
}
- /// <summary>
- /// Returns the folder containing the item.
- /// If the item is a folder, it returns the folder itself.
- /// </summary>
- /// <value>The containing folder path.</value>
- [JsonIgnore]
- public override string ContainingFolderPath => Path;
-
- [JsonIgnore]
- public override bool IsDisplayedAsFolder => true;
-
- [JsonIgnore]
- public override bool SupportsAncestors => false;
-
public override bool IsSaveLocalMetadataEnabled()
{
return true;
@@ -70,9 +75,6 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query);
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
public static string GetPath(string name)
{
return GetPath(name, true);
@@ -106,11 +108,13 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// This is called before any metadata refresh and returns true or false indicating if changes were made.
+ /// This is called before any metadata refresh and returns true if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
+ /// <returns>true if the item has change, else false.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
index b84a9fa6f..2304570fd 100644
--- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs
index d7d007668..3aeb7468f 100644
--- a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs
+++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
namespace MediaBrowser.Controller.Entities
{
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
index 13226b234..14459624e 100644
--- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
+++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
namespace MediaBrowser.Controller.Entities
{
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
index 0f612262a..98c3b3edf 100644
--- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs
+++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
index f747b5149..f80f7c304 100644
--- a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
+++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.LiveTv;
diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
index b027a0cb1..ae01c223e 100644
--- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs
+++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
@@ -1,7 +1,7 @@
namespace MediaBrowser.Controller.Entities
{
/// <summary>
- /// Interface IHasScreenshots.
+ /// The item has screenshots.
/// </summary>
public interface IHasScreenshots
{
diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs
index 5444f1f52..5f774bbde 100644
--- a/MediaBrowser.Controller/Entities/IHasSeries.cs
+++ b/MediaBrowser.Controller/Entities/IHasSeries.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,7 +9,7 @@ namespace MediaBrowser.Controller.Entities
public interface IHasSeries
{
/// <summary>
- /// Gets the name of the series.
+ /// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
string SeriesName { get; set; }
diff --git a/MediaBrowser.Controller/Entities/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs
new file mode 100644
index 000000000..bdde744a3
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasShares.cs
@@ -0,0 +1,11 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasShares
+ {
+ Share[] Shares { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
index 6a350212b..f317a02ff 100644
--- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
+++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs
index d1f6f2b7e..2bd9ded33 100644
--- a/MediaBrowser.Controller/Entities/IHasTrailers.cs
+++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 270217356..ebaf5506d 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -18,9 +18,9 @@ namespace MediaBrowser.Controller.Entities
public int? Limit { get; set; }
- public User User { get; set; }
+ public User? User { get; set; }
- public BaseItem SimilarTo { get; set; }
+ public BaseItem? SimilarTo { get; set; }
public bool? IsFolder { get; set; }
@@ -56,23 +56,23 @@ namespace MediaBrowser.Controller.Entities
public bool? CollapseBoxSetItems { get; set; }
- public string NameStartsWithOrGreater { get; set; }
+ public string? NameStartsWithOrGreater { get; set; }
- public string NameStartsWith { get; set; }
+ public string? NameStartsWith { get; set; }
- public string NameLessThan { get; set; }
+ public string? NameLessThan { get; set; }
- public string NameContains { get; set; }
+ public string? NameContains { get; set; }
- public string MinSortName { get; set; }
+ public string? MinSortName { get; set; }
- public string PresentationUniqueKey { get; set; }
+ public string? PresentationUniqueKey { get; set; }
- public string Path { get; set; }
+ public string? Path { get; set; }
- public string Name { get; set; }
+ public string? Name { get; set; }
- public string Person { get; set; }
+ public string? Person { get; set; }
public Guid[] PersonIds { get; set; }
@@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.Entities
public Guid[] ExcludeItemIds { get; set; }
- public string AdjacentTo { get; set; }
+ public string? AdjacentTo { get; set; }
public string[] PersonTypes { get; set; }
@@ -180,13 +180,13 @@ namespace MediaBrowser.Controller.Entities
public Guid ParentId { get; set; }
- public string ParentType { get; set; }
+ public string? ParentType { get; set; }
public Guid[] AncestorIds { get; set; }
public Guid[] TopParentIds { get; set; }
- public BaseItem Parent
+ public BaseItem? Parent
{
set
{
@@ -211,9 +211,9 @@ namespace MediaBrowser.Controller.Entities
public SeriesStatus[] SeriesStatuses { get; set; }
- public string ExternalSeriesId { get; set; }
+ public string? ExternalSeriesId { get; set; }
- public string ExternalId { get; set; }
+ public string? ExternalId { get; set; }
public Guid[] AlbumIds { get; set; }
@@ -221,9 +221,9 @@ namespace MediaBrowser.Controller.Entities
public Guid[] ExcludeArtistIds { get; set; }
- public string AncestorWithPresentationUniqueKey { get; set; }
+ public string? AncestorWithPresentationUniqueKey { get; set; }
- public string SeriesPresentationUniqueKey { get; set; }
+ public string? SeriesPresentationUniqueKey { get; set; }
public bool GroupByPresentationUniqueKey { get; set; }
@@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Entities
public bool ForceDirect { get; set; }
- public Dictionary<string, string> ExcludeProviderIds { get; set; }
+ public Dictionary<string, string>? ExcludeProviderIds { get; set; }
public bool EnableGroupByMetadataKey { get; set; }
@@ -251,13 +251,13 @@ namespace MediaBrowser.Controller.Entities
public int MinSimilarityScore { get; set; }
- public string HasNoAudioTrackWithLanguage { get; set; }
+ public string? HasNoAudioTrackWithLanguage { get; set; }
- public string HasNoInternalSubtitleTrackWithLanguage { get; set; }
+ public string? HasNoInternalSubtitleTrackWithLanguage { get; set; }
- public string HasNoExternalSubtitleTrackWithLanguage { get; set; }
+ public string? HasNoExternalSubtitleTrackWithLanguage { get; set; }
- public string HasNoSubtitleTrackWithLanguage { get; set; }
+ public string? HasNoSubtitleTrackWithLanguage { get; set; }
public bool? IsDeadArtist { get; set; }
@@ -265,6 +265,11 @@ namespace MediaBrowser.Controller.Entities
public bool? IsDeadPerson { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether album sub-folders should be returned if they exist.
+ /// </summary>
+ public bool? DisplayAlbumFolders { get; set; }
+
public InternalItemsQuery()
{
AlbumArtistIds = Array.Empty<Guid>();
@@ -281,12 +286,10 @@ namespace MediaBrowser.Controller.Entities
ExcludeInheritedTags = Array.Empty<string>();
ExcludeItemIds = Array.Empty<Guid>();
ExcludeItemTypes = Array.Empty<string>();
- ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
ExcludeTags = Array.Empty<string>();
GenreIds = Array.Empty<Guid>();
Genres = Array.Empty<string>();
GroupByPresentationUniqueKey = true;
- HasAnyProviderId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
ImageTypes = Array.Empty<ImageType>();
IncludeItemTypes = Array.Empty<string>();
ItemIds = Array.Empty<Guid>();
@@ -307,32 +310,33 @@ namespace MediaBrowser.Controller.Entities
Years = Array.Empty<int>();
}
- public InternalItemsQuery(User user)
+ public InternalItemsQuery(User? user)
: this()
{
- SetUser(user);
+ if (user != null)
+ {
+ SetUser(user);
+ }
}
public void SetUser(User user)
{
- if (user != null)
- {
- MaxParentalRating = user.MaxParentalAgeRating;
+ MaxParentalRating = user.MaxParentalAgeRating;
- if (MaxParentalRating.HasValue)
- {
- BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
- .Where(i => i != UnratedItem.Other.ToString())
- .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
- }
+ if (MaxParentalRating.HasValue)
+ {
+ string other = UnratedItem.Other.ToString();
+ BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
+ .Where(i => i != other)
+ .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+ }
- ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
+ ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
- User = user;
- }
+ User = user;
}
- public Dictionary<string, string> HasAnyProviderId { get; set; }
+ public Dictionary<string, string>? HasAnyProviderId { get; set; }
public Guid[] AlbumArtistIds { get; set; }
@@ -354,8 +358,8 @@ namespace MediaBrowser.Controller.Entities
public int? MinWidth { get; set; }
- public string SearchTerm { get; set; }
+ public string? SearchTerm { get; set; }
- public string SeriesTimerId { get; set; }
+ public string? SeriesTimerId { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
index 5b96a5af6..3e1d89274 100644
--- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -1,12 +1,26 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Entities
{
public class InternalPeopleQuery
{
+ public InternalPeopleQuery()
+ : this(Array.Empty<string>(), Array.Empty<string>())
+ {
+ }
+
+ public InternalPeopleQuery(IReadOnlyList<string> personTypes, IReadOnlyList<string> excludePersonTypes)
+ {
+ PersonTypes = personTypes;
+ ExcludePersonTypes = excludePersonTypes;
+ }
+
/// <summary>
/// Gets or sets the maximum number of items the query should return.
/// </summary>
@@ -14,9 +28,9 @@ namespace MediaBrowser.Controller.Entities
public Guid ItemId { get; set; }
- public string[] PersonTypes { get; set; }
+ public IReadOnlyList<string> PersonTypes { get; }
- public string[] ExcludePersonTypes { get; set; }
+ public IReadOnlyList<string> ExcludePersonTypes { get; }
public int? MaxListOrder { get; set; }
@@ -27,11 +41,5 @@ namespace MediaBrowser.Controller.Entities
public User User { get; set; }
public bool? IsFavorite { get; set; }
-
- public InternalPeopleQuery()
- {
- PersonTypes = Array.Empty<string>();
- ExcludePersonTypes = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
index 570d8eec0..ea8555dbf 100644
--- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs
+++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs
index 8e0f721e7..fd5fef3dc 100644
--- a/MediaBrowser.Controller/Entities/LinkedChild.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChild.cs
@@ -1,15 +1,20 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.Text.Json.Serialization;
-using MediaBrowser.Model.IO;
namespace MediaBrowser.Controller.Entities
{
public class LinkedChild
{
+ public LinkedChild()
+ {
+ Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ }
+
public string Path { get; set; }
public LinkedChildType Type { get; set; }
@@ -20,7 +25,7 @@ namespace MediaBrowser.Controller.Entities
public string Id { get; set; }
/// <summary>
- /// Serves as a cache.
+ /// Gets or sets the linked item id.
/// </summary>
public Guid? ItemId { get; set; }
@@ -39,41 +44,5 @@ namespace MediaBrowser.Controller.Entities
return child;
}
-
- public LinkedChild()
- {
- Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- }
- }
-
- public enum LinkedChildType
- {
- Manual = 0,
- Shortcut = 1
- }
-
- public class LinkedChildComparer : IEqualityComparer<LinkedChild>
- {
- private readonly IFileSystem _fileSystem;
-
- public LinkedChildComparer(IFileSystem fileSystem)
- {
- _fileSystem = fileSystem;
- }
-
- public bool Equals(LinkedChild x, LinkedChild y)
- {
- if (x.Type == y.Type)
- {
- return _fileSystem.AreEqual(x.Path, y.Path);
- }
-
- return false;
- }
-
- public int GetHashCode(LinkedChild obj)
- {
- return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode();
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
new file mode 100644
index 000000000..4e58e2942
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
@@ -0,0 +1,35 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class LinkedChildComparer : IEqualityComparer<LinkedChild>
+ {
+ private readonly IFileSystem _fileSystem;
+
+ public LinkedChildComparer(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ public bool Equals(LinkedChild x, LinkedChild y)
+ {
+ if (x.Type == y.Type)
+ {
+ return _fileSystem.AreEqual(x.Path, y.Path);
+ }
+
+ return false;
+ }
+
+ public int GetHashCode(LinkedChild obj)
+ {
+ return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/LinkedChildType.cs b/MediaBrowser.Controller/Entities/LinkedChildType.cs
new file mode 100644
index 000000000..9ddb7b620
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/LinkedChildType.cs
@@ -0,0 +1,18 @@
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// The linked child type.
+ /// </summary>
+ public enum LinkedChildType
+ {
+ /// <summary>
+ /// Manually linked child.
+ /// </summary>
+ Manual = 0,
+
+ /// <summary>
+ /// Shortcut linked child.
+ /// </summary>
+ Shortcut = 1
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 05e4229ca..74e84288d 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -217,8 +219,7 @@ namespace MediaBrowser.Controller.Entities.Movies
private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders)
{
- var boxset = item as BoxSet;
- if (boxset != null)
+ if (item is BoxSet boxset)
{
if (!expandedFolders.Contains(item.Id))
{
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index 8b67aaccc..b54bbf5eb 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -142,9 +144,9 @@ namespace MediaBrowser.Controller.Entities.Movies
}
/// <inheritdoc />
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!ProductionYear.HasValue)
{
diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs
index b278a0142..237ad5198 100644
--- a/MediaBrowser.Controller/Entities/MusicVideo.cs
+++ b/MediaBrowser.Controller/Entities/MusicVideo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,15 +13,15 @@ namespace MediaBrowser.Controller.Entities
{
public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo>
{
- /// <inheritdoc />
- [JsonIgnore]
- public IReadOnlyList<string> Artists { get; set; }
-
public MusicVideo()
{
Artists = Array.Empty<string>();
}
+ /// <inheritdoc />
+ [JsonIgnore]
+ public IReadOnlyList<string> Artists { get; set; }
+
public override UnratedItem GetBlockUnratedType()
{
return UnratedItem.Music;
@@ -34,9 +36,9 @@ namespace MediaBrowser.Controller.Entities
return info;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!ProductionYear.HasValue)
{
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
index 1f3758a73..687ce1ec8 100644
--- a/MediaBrowser.Controller/Entities/PeopleHelper.cs
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -100,23 +100,5 @@ namespace MediaBrowser.Controller.Entities
existing.SetProviderId(id.Key, id.Value);
}
}
-
- public static bool ContainsPerson(List<PersonInfo> people, string name)
- {
- if (string.IsNullOrEmpty(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
-
- foreach (var i in people)
- {
- if (string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
index c4fcb0267..913f76d3b 100644
--- a/MediaBrowser.Controller/Entities/Person.cs
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -48,7 +50,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
@@ -65,6 +67,9 @@ namespace MediaBrowser.Controller.Entities
return true;
}
+ /// <summary>
+ /// Gets a value indicating whether to enable alpha numeric sorting.
+ /// </summary>
[JsonIgnore]
public override bool EnableAlphaNumericSorting => false;
@@ -124,9 +129,9 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs
index 4ff9b0955..fb79323f8 100644
--- a/MediaBrowser.Controller/Entities/PersonInfo.cs
+++ b/MediaBrowser.Controller/Entities/PersonInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs
index 2fc66176f..3312a0e3e 100644
--- a/MediaBrowser.Controller/Entities/Photo.cs
+++ b/MediaBrowser.Controller/Entities/Photo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Text.Json.Serialization;
@@ -24,8 +26,7 @@ namespace MediaBrowser.Controller.Entities
var parents = GetParents();
foreach (var parent in parents)
{
- var photoAlbum = parent as PhotoAlbum;
- if (photoAlbum != null)
+ if (parent is PhotoAlbum photoAlbum)
{
return photoAlbum;
}
diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs
index 50f1655f3..64f446eef 100644
--- a/MediaBrowser.Controller/Entities/Share.cs
+++ b/MediaBrowser.Controller/Entities/Share.cs
@@ -1,12 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Entities
{
- public interface IHasShares
- {
- Share[] Shares { get; set; }
- }
-
public class Share
{
public string UserId { get; set; }
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs
index 9018ddb75..6fd0a6c6c 100644
--- a/MediaBrowser.Controller/Entities/Studio.cs
+++ b/MediaBrowser.Controller/Entities/Studio.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -27,7 +29,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
@@ -103,9 +105,9 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index dc12fbbea..31c179bca 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -32,7 +34,7 @@ namespace MediaBrowser.Controller.Entities.TV
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
/// <summary>
- /// Gets the season in which it aired.
+ /// Gets or sets the season in which it aired.
/// </summary>
/// <value>The aired season.</value>
public int? AirsBeforeSeasonNumber { get; set; }
@@ -42,7 +44,7 @@ namespace MediaBrowser.Controller.Entities.TV
public int? AirsBeforeEpisodeNumber { get; set; }
/// <summary>
- /// This is the ending episode number for double episodes.
+ /// Gets or sets the ending episode number for double episodes.
/// </summary>
/// <value>The index number.</value>
public int? IndexNumberEnd { get; set; }
@@ -99,14 +101,22 @@ namespace MediaBrowser.Controller.Entities.TV
take--;
}
- list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000")));
+ var newList = seriesUserDataKeys.GetRange(0, take);
+ var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
+ for (int i = 0; i < take; i++)
+ {
+ newList[i] = newList[i] + suffix;
+ }
+
+ newList.AddRange(list);
+ list = newList;
}
return list;
}
/// <summary>
- /// This Episode's Series Instance.
+ /// Gets the Episode's Series Instance.
/// </summary>
/// <value>The series.</value>
[JsonIgnore]
@@ -208,8 +218,8 @@ namespace MediaBrowser.Controller.Entities.TV
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "")
- + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty)
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
}
/// <summary>
@@ -251,6 +261,7 @@ namespace MediaBrowser.Controller.Entities.TV
[JsonIgnore]
public Guid SeasonId { get; set; }
+
[JsonIgnore]
public Guid SeriesId { get; set; }
@@ -276,7 +287,8 @@ namespace MediaBrowser.Controller.Entities.TV
public override IEnumerable<FileSystemMetadata> GetDeletePaths()
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = Path,
@@ -308,9 +320,9 @@ namespace MediaBrowser.Controller.Entities.TV
return id;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!IsLocked)
{
@@ -318,7 +330,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
try
{
- if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetdata))
+ if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata))
{
hasChanges = true;
}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 93bdd6e70..aa62bb35b 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -1,7 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
@@ -56,7 +59,15 @@ namespace MediaBrowser.Controller.Entities.TV
var series = Series;
if (series != null)
{
- list.InsertRange(0, series.GetUserDataKeys().Select(i => i + (IndexNumber ?? 0).ToString("000")));
+ var newList = series.GetUserDataKeys();
+ var suffix = (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture);
+ for (int i = 0; i < newList.Count; i++)
+ {
+ newList[i] = newList[i] + suffix;
+ }
+
+ newList.AddRange(list);
+ list = newList;
}
return list;
@@ -70,7 +81,7 @@ namespace MediaBrowser.Controller.Entities.TV
}
/// <summary>
- /// This Episode's Series Instance.
+ /// Gets this Episode's Series Instance.
/// </summary>
/// <value>The series.</value>
[JsonIgnore]
@@ -111,7 +122,7 @@ namespace MediaBrowser.Controller.Entities.TV
var series = Series;
if (series != null)
{
- return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000");
+ return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture);
}
}
@@ -124,7 +135,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name;
+ return IndexNumber != null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name;
}
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
@@ -231,9 +242,9 @@ namespace MediaBrowser.Controller.Entities.TV
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
{
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 1a379074d..44d07b4a4 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -57,8 +59,11 @@ namespace MediaBrowser.Controller.Entities.TV
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
/// <summary>
- /// airdate, dvd or absolute.
+ /// Gets or sets the display order.
/// </summary>
+ /// <remarks>
+ /// Valid options are airdate, dvd or absolute.
+ /// </remarks>
public string DisplayOrder { get; set; }
/// <summary>
@@ -107,7 +112,7 @@ namespace MediaBrowser.Controller.Entities.TV
return key;
}
- return key + "-" + string.Join("-", folders);
+ return key + "-" + string.Join('-', folders);
}
private static string GetUniqueSeriesKey(BaseItem series)
@@ -169,14 +174,12 @@ namespace MediaBrowser.Controller.Entities.TV
{
var list = base.GetUserDataKeys();
- var key = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(key))
+ if (this.TryGetProviderId(MetadataProvider.Imdb, out var key))
{
list.Insert(0, key);
}
- key = this.GetProviderId(MetadataProvider.Tvdb);
- if (!string.IsNullOrEmpty(key))
+ if (this.TryGetProviderId(MetadataProvider.Tvdb, out key))
{
list.Insert(0, key);
}
@@ -208,7 +211,7 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
query.IncludeItemTypes = new[] { nameof(Season) };
- query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
+ query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
if (user != null && !user.DisplayMissingEpisodes)
{
@@ -228,7 +231,7 @@ namespace MediaBrowser.Controller.Entities.TV
query.SeriesPresentationUniqueKey = seriesKey;
if (query.OrderBy.Count == 0)
{
- query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
+ query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
}
if (query.IncludeItemTypes.Length == 0)
@@ -254,7 +257,7 @@ namespace MediaBrowser.Controller.Entities.TV
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { nameof(Episode), nameof(Season) },
- OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
@@ -318,20 +321,13 @@ namespace MediaBrowser.Controller.Entities.TV
cancellationToken.ThrowIfCancellationRequested();
- var skipItem = false;
-
- var episode = item as Episode;
-
- if (episode != null
+ bool skipItem = item is Episode episode
&& refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh
&& !refreshOptions.ReplaceAllMetadata
&& episode.IsMissingEpisode
&& episode.LocationType == LocationType.Virtual
&& episode.PremiereDate.HasValue
- && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30)
- {
- skipItem = true;
- }
+ && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30;
if (!skipItem)
{
@@ -365,7 +361,7 @@ namespace MediaBrowser.Controller.Entities.TV
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
IncludeItemTypes = new[] { nameof(Episode) },
- OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
if (user != null)
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 9ae8ad708..732b45521 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -43,9 +45,9 @@ namespace MediaBrowser.Controller.Entities
return info;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!ProductionYear.HasValue)
{
diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs
index db63c42e4..6ab2116d7 100644
--- a/MediaBrowser.Controller/Entities/UserItemData.cs
+++ b/MediaBrowser.Controller/Entities/UserItemData.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -94,7 +96,7 @@ namespace MediaBrowser.Controller.Entities
public const double MinLikeValue = 6.5;
/// <summary>
- /// This is an interpreted property to indicate likes or dislikes
+ /// Gets or sets a value indicating whether the item is liked or not.
/// This should never be serialized.
/// </summary>
/// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index 7f7224ae0..2b15a52f0 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -21,6 +23,7 @@ namespace MediaBrowser.Controller.Entities
{
private List<Guid> _childrenIds = null;
private readonly object _childIdsLock = new object();
+
protected override List<BaseItem> LoadChildren()
{
lock (_childIdsLock)
@@ -85,10 +88,10 @@ namespace MediaBrowser.Controller.Entities
return list;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
ClearCache();
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase))
{
@@ -106,11 +109,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index b1da4d64c..57dc9b59b 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -13,13 +15,19 @@ namespace MediaBrowser.Controller.Entities
{
public class UserView : Folder, IHasCollectionType
{
- /// <inheritdoc />
+ /// <summary>
+ /// Gets or sets the view type.
+ /// </summary>
public string ViewType { get; set; }
- /// <inheritdoc />
+ /// <summary>
+ /// Gets or sets the display parent id.
+ /// </summary>
public new Guid DisplayParentId { get; set; }
- /// <inheritdoc />
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
public Guid? UserId { get; set; }
public static ITVSeriesManager TVSeriesManager;
@@ -75,10 +83,7 @@ namespace MediaBrowser.Controller.Entities
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
- if (query == null)
- {
- query = new InternalItemsQuery(user);
- }
+ query ??= new InternalItemsQuery(user);
query.EnableTotalRecordCount = false;
var result = GetItemList(query);
@@ -111,10 +116,10 @@ namespace MediaBrowser.Controller.Entities
return GetChildren(user, false);
}
- private static string[] UserSpecificViewTypes = new string[]
- {
- Model.Entities.CollectionType.Playlists
- };
+ private static readonly string[] UserSpecificViewTypes = new string[]
+ {
+ Model.Entities.CollectionType.Playlists
+ };
public static bool IsUserSpecific(Folder folder)
{
@@ -167,7 +172,7 @@ namespace MediaBrowser.Controller.Entities
return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 4e33a6bbd..add734f62 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -53,12 +55,12 @@ namespace MediaBrowser.Controller.Entities
// if (query.IncludeItemTypes != null &&
// query.IncludeItemTypes.Length == 1 &&
// string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase))
- //{
+ // {
// if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
// {
// return await FindPlaylists(queryParent, user, query).ConfigureAwait(false);
// }
- //}
+ // }
switch (viewType)
{
@@ -217,8 +219,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
-
+ query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
@@ -230,7 +231,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
+ query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
query.IsResumable = true;
query.Recursive = true;
query.Parent = parent;
@@ -327,8 +328,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
-
+ query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
@@ -344,19 +344,21 @@ namespace MediaBrowser.Controller.Entities
var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty });
var result = _tvSeriesManager.GetNextUp(
- new NextUpQuery
- {
- Limit = query.Limit,
- StartIndex = query.StartIndex,
- UserId = query.User.Id
- }, parentFolders, query.DtoOptions);
+ new NextUpQuery
+ {
+ Limit = query.Limit,
+ StartIndex = query.StartIndex,
+ UserId = query.User.Id
+ },
+ parentFolders,
+ query.DtoOptions);
return result;
}
private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
+ query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
query.IsResumable = true;
query.Recursive = true;
query.Parent = parent;
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 6320b01b8..d05b5df2f 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -480,7 +482,8 @@ namespace MediaBrowser.Controller.Entities
{
if (!IsInMixedFolder)
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = ContainingFolderPath,
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
index b2e4d307a..f268bc939 100644
--- a/MediaBrowser.Controller/Entities/Year.cs
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -22,7 +24,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
@@ -110,11 +112,13 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// This is called before any metadata refresh and returns true or false indicating if changes were made.
+ /// This is called before any metadata refresh and returns true if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
+ /// <returns>true if the item has change, else false.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Events/IEventConsumer.cs b/MediaBrowser.Controller/Events/IEventConsumer.cs
index 5c4ab5d8d..93005134a 100644
--- a/MediaBrowser.Controller/Events/IEventConsumer.cs
+++ b/MediaBrowser.Controller/Events/IEventConsumer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Events
diff --git a/MediaBrowser.Controller/Events/IEventManager.cs b/MediaBrowser.Controller/Events/IEventManager.cs
index a1f40b3a6..074e3f1fe 100644
--- a/MediaBrowser.Controller/Events/IEventManager.cs
+++ b/MediaBrowser.Controller/Events/IEventManager.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Events
diff --git a/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs
index 46d7e5a17..3a331ad00 100644
--- a/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Session;
namespace MediaBrowser.Controller.Events.Session
diff --git a/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs
index aab19cc46..deeaaf55d 100644
--- a/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Session;
namespace MediaBrowser.Controller.Events.Session
diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs
index b06046c05..0dd8b0dbf 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs
index dfadc9f61..c1d503a7e 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs
index 045a60027..7a9866834 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
index a111e6d82..0f27be9bb 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
@@ -1,5 +1,4 @@
using Jellyfin.Data.Events;
-using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs
index 661ca066a..b078e06dc 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs
index 182c8ef65..f1af01345 100644
--- a/MediaBrowser.Controller/Extensions/StringExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System;
@@ -22,6 +21,27 @@ namespace MediaBrowser.Controller.Extensions
return Normalize(string.Concat(chars), NormalizationForm.FormC);
}
+ /// <summary>
+ /// Counts the number of occurrences of [needle] in the string.
+ /// </summary>
+ /// <param name="value">The haystack to search in.</param>
+ /// <param name="needle">The character to search for.</param>
+ /// <returns>The number of occurrences of the [needle] character.</returns>
+ public static int Count(this ReadOnlySpan<char> value, char needle)
+ {
+ var count = 0;
+ var length = value.Length;
+ for (var i = 0; i < length; i++)
+ {
+ if (value[i] == needle)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true)
{
if (stripStringOnFailure)
@@ -34,7 +54,7 @@ namespace MediaBrowser.Controller.Extensions
{
// will throw if input contains invalid unicode chars
// https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
- text = Regex.Replace(text, "([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])", "");
+ text = Regex.Replace(text, "([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])", string.Empty);
return Normalize(text, form, false);
}
}
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
index 041eeea62..1678d5067 100644
--- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using Jellyfin.Data.Entities;
@@ -48,7 +50,7 @@ namespace MediaBrowser.Controller
/// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
/// <returns>The dictionary of custom item display preferences.</returns>
- IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client);
+ Dictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client);
/// <summary>
/// Sets the custom item display preference for the user and client.
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
index 3db60ae0b..b8a0bf331 100644
--- a/MediaBrowser.Controller/IO/FileData.cs
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.IO
/// <param name="flattenFolderDepth">The flatten folder depth.</param>
/// <param name="resolveShortcuts">if set to <c>true</c> [resolve shortcuts].</param>
/// <returns>Dictionary{System.StringFileSystemInfo}.</returns>
- /// <exception cref="ArgumentNullException">path</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c> or empty.</exception>
public static FileSystemMetadata[] GetFilteredFileSystemEntries(
IDirectoryService directoryService,
string path,
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 92b2d43ce..094923842 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -1,12 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
using MediaBrowser.Common;
-using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
@@ -53,6 +52,11 @@ namespace MediaBrowser.Controller
string FriendlyName { get; }
/// <summary>
+ /// Gets the configured published server url.
+ /// </summary>
+ string PublishedServerUrl { get; }
+
+ /// <summary>
/// Gets the system info.
/// </summary>
/// <param name="source">The originator of the request.</param>
diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs
index be57d6bca..1890dbb36 100644
--- a/MediaBrowser.Controller/IServerApplicationPaths.cs
+++ b/MediaBrowser.Controller/IServerApplicationPaths.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Common.Configuration;
diff --git a/MediaBrowser.Controller/Library/DeleteOptions.cs b/MediaBrowser.Controller/Library/DeleteOptions.cs
index b7417efcb..408e70284 100644
--- a/MediaBrowser.Controller/Library/DeleteOptions.cs
+++ b/MediaBrowser.Controller/Library/DeleteOptions.cs
@@ -4,13 +4,13 @@ namespace MediaBrowser.Controller.Library
{
public class DeleteOptions
{
- public bool DeleteFileLocation { get; set; }
-
- public bool DeleteFromExternalProvider { get; set; }
-
public DeleteOptions()
{
DeleteFromExternalProvider = true;
}
+
+ public bool DeleteFileLocation { get; set; }
+
+ public bool DeleteFromExternalProvider { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs
index d45493d40..a74d1b9f0 100644
--- a/MediaBrowser.Controller/Library/IIntroProvider.cs
+++ b/MediaBrowser.Controller/Library/IIntroProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -10,6 +12,12 @@ namespace MediaBrowser.Controller.Library
public interface IIntroProvider
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
/// Gets the intros.
/// </summary>
/// <param name="item">The item.</param>
@@ -22,11 +30,5 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <returns>IEnumerable{System.String}.</returns>
IEnumerable<string> GetAllIntroFiles();
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- string Name { get; }
}
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 6700761fc..7a4ba6a24 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -1,9 +1,12 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Naming.Common;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -41,6 +44,12 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Resolves a set of files into a list of BaseItem.
/// </summary>
+ /// <param name="files">The list of tiles.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ /// <param name="parent">The parent folder.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="collectionType">The collection type.</param>
+ /// <returns>The items resolved from the paths.</returns>
IEnumerable<BaseItem> ResolvePaths(
IEnumerable<FileSystemMetadata> files,
IDirectoryService directoryService,
@@ -344,6 +353,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
/// <param name="uniqueId">The unique identifier.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
string name,
Guid parentId,
@@ -357,10 +367,11 @@ namespace MediaBrowser.Controller.Library
/// <param name="parent">The parent.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The shadow view.</returns>
UserView GetShadowView(
BaseItem parent,
- string viewType,
- string sortName);
+ string viewType,
+ string sortName);
/// <summary>
/// Determines whether [is video file] [the specified path].
@@ -467,6 +478,15 @@ namespace MediaBrowser.Controller.Library
void UpdatePeople(BaseItem item, List<PersonInfo> people);
/// <summary>
+ /// Asynchronously updates the people.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="people">The people.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The async task.</returns>
+ Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken);
+
+ /// <summary>
/// Gets the item ids.
/// </summary>
/// <param name="query">The query.</param>
@@ -542,7 +562,7 @@ namespace MediaBrowser.Controller.Library
Guid GetMusicGenreId(string name);
- Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary);
+ Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary);
Task RemoveVirtualFolder(string name, bool refreshLibrary);
@@ -576,5 +596,11 @@ namespace MediaBrowser.Controller.Library
BaseItem GetParentItem(string parentId, Guid? userId);
BaseItem GetParentItem(Guid? parentId, Guid? userId);
+
+ /// <summary>
+ /// Gets or creates a static instance of <see cref="NamingOptions"/>.
+ /// </summary>
+ /// <returns>An instance of the <see cref="NamingOptions"/> class.</returns>
+ NamingOptions GetNamingOptions();
}
}
diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs
index ff25be657..85d866de5 100644
--- a/MediaBrowser.Controller/Library/ILiveStream.cs
+++ b/MediaBrowser.Controller/Library/ILiveStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Threading;
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 21c6ef2af..d3d85a056 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Library/IMetadataSaver.cs b/MediaBrowser.Controller/Library/IMetadataSaver.cs
index 027cc5b40..5fbfad881 100644
--- a/MediaBrowser.Controller/Library/IMetadataSaver.cs
+++ b/MediaBrowser.Controller/Library/IMetadataSaver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
index d12f008e7..5329841bf 100644
--- a/MediaBrowser.Controller/Library/IMusicManager.cs
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index c6a83e4dc..e5dcfcff0 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -47,17 +49,16 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Get all user data for the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <returns>The user item data.</returns>
List<UserItemData> GetAllUserData(Guid userId);
/// <summary>
/// Save the all provided user data for the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <param name="userData"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userData">The array of user data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken);
/// <summary>
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 6e267834b..1801b1c41 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -59,16 +61,16 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <param name="newName">The new name.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
Task RenameUser(User user, string newName);
/// <summary>
/// Updates the user.
/// </summary>
/// <param name="user">The user.</param>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
void UpdateUser(User user);
/// <summary>
@@ -85,8 +87,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="name">The name of the new user.</param>
/// <returns>The created user.</returns>
- /// <exception cref="ArgumentNullException">name</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception>
+ /// <exception cref="ArgumentException"><paramref name="name"/> already exists.</exception>
Task<User> CreateUserAsync(string name);
/// <summary>
diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs
index 8d541e8b6..46004e42f 100644
--- a/MediaBrowser.Controller/Library/IUserViewManager.cs
+++ b/MediaBrowser.Controller/Library/IUserViewManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Library/IntroInfo.cs b/MediaBrowser.Controller/Library/IntroInfo.cs
index 283cc631c..90786786b 100644
--- a/MediaBrowser.Controller/Library/IntroInfo.cs
+++ b/MediaBrowser.Controller/Library/IntroInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
index 1798a4fad..a37dc7af1 100644
--- a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 12a311dc3..521e37274 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -14,14 +16,14 @@ namespace MediaBrowser.Controller.Library
/// These are arguments relating to the file system that are collected once and then referred to
/// whenever needed. Primarily for entity resolution.
/// </summary>
- public class ItemResolveArgs : EventArgs
+ public class ItemResolveArgs
{
/// <summary>
/// The _app paths.
/// </summary>
private readonly IServerApplicationPaths _appPaths;
- public IDirectoryService DirectoryService { get; private set; }
+ private LibraryOptions _libraryOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ItemResolveArgs" /> class.
@@ -34,17 +36,18 @@ namespace MediaBrowser.Controller.Library
DirectoryService = directoryService;
}
+ public IDirectoryService DirectoryService { get; }
+
/// <summary>
- /// Gets the file system children.
+ /// Gets or sets the file system children.
/// </summary>
/// <value>The file system children.</value>
public FileSystemMetadata[] FileSystemChildren { get; set; }
- public LibraryOptions LibraryOptions { get; set; }
-
- public LibraryOptions GetLibraryOptions()
+ public LibraryOptions LibraryOptions
{
- return LibraryOptions ?? (LibraryOptions = Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent));
+ get => _libraryOptions ??= Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent);
+ set => _libraryOptions = value;
}
/// <summary>
@@ -60,10 +63,10 @@ namespace MediaBrowser.Controller.Library
public FileSystemMetadata FileInfo { get; set; }
/// <summary>
- /// Gets or sets the path.
+ /// Gets the path.
/// </summary>
/// <value>The path.</value>
- public string Path { get; set; }
+ public string Path => FileInfo.FullName;
/// <summary>
/// Gets a value indicating whether this instance is directory.
@@ -139,7 +142,7 @@ namespace MediaBrowser.Controller.Library
/// Adds the additional location.
/// </summary>
/// <param name="path">The path.</param>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
public void AddAdditionalLocation(string path)
{
if (string.IsNullOrEmpty(path))
@@ -147,11 +150,7 @@ namespace MediaBrowser.Controller.Library
throw new ArgumentException("The path was empty or null.", nameof(path));
}
- if (AdditionalLocations == null)
- {
- AdditionalLocations = new List<string>();
- }
-
+ AdditionalLocations ??= new List<string>();
AdditionalLocations.Add(path);
}
@@ -175,7 +174,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="name">The name.</param>
/// <returns>FileSystemInfo.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception>
public FileSystemMetadata GetFileSystemEntryByName(string name)
{
if (string.IsNullOrEmpty(name))
@@ -243,14 +242,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
public override int GetHashCode()
{
- return Path.GetHashCode();
+ return Path.GetHashCode(StringComparison.Ordinal);
}
/// <summary>
/// Equals the specified args.
/// </summary>
/// <param name="args">The args.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <returns><c>true</c> if the arguments are the same, <c>false</c> otherwise.</returns>
protected bool Equals(ItemResolveArgs args)
{
if (args != null)
diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
index 9581603f0..7bc8fa5ab 100644
--- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
+++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs
new file mode 100644
index 000000000..41cfcae16
--- /dev/null
+++ b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs
@@ -0,0 +1,17 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Library
+{
+ public static class MetadataConfigurationExtensions
+ {
+ public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config)
+ {
+ return config.GetConfiguration<MetadataConfiguration>("metadata");
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs
index f16304db0..a6be6c0d3 100644
--- a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs
+++ b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs
@@ -14,18 +14,10 @@ namespace MediaBrowser.Controller.Library
{
new ConfigurationStore
{
- Key = "metadata",
- ConfigurationType = typeof(MetadataConfiguration)
+ Key = "metadata",
+ ConfigurationType = typeof(MetadataConfiguration)
}
};
}
}
-
- public static class MetadataConfigurationExtensions
- {
- public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config)
- {
- return config.GetConfiguration<MetadataConfiguration>("metadata");
- }
- }
}
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
index 1c90bb4e0..29bfeca09 100644
--- a/MediaBrowser.Controller/Library/NameExtensions.cs
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System;
@@ -10,6 +9,10 @@ namespace MediaBrowser.Controller.Library
{
public static class NameExtensions
{
+ public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
+ => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
+ .Select(x => x.First());
+
private static string RemoveDiacritics(string? name)
{
if (name == null)
@@ -19,9 +22,5 @@ namespace MediaBrowser.Controller.Library
return name.RemoveDiacritics();
}
-
- public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
- => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First());
}
}
diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
index a2be3a42a..609336ec4 100644
--- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
+++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs
index ac372bceb..2138fef58 100644
--- a/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs
+++ b/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs
@@ -1,4 +1,4 @@
-namespace MediaBrowser.Controller.Library
+namespace MediaBrowser.Controller.Library
{
/// <summary>
/// An event that occurs when playback is started.
diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs
deleted file mode 100644
index 5efdc6a48..000000000
--- a/MediaBrowser.Controller/Library/Profiler.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Globalization;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Controller.Library
-{
- /// <summary>
- /// Class Profiler.
- /// </summary>
- public class Profiler : IDisposable
- {
- /// <summary>
- /// The name.
- /// </summary>
- readonly string _name;
-
- /// <summary>
- /// The stopwatch.
- /// </summary>
- readonly Stopwatch _stopwatch;
-
- /// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger<Profiler> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Profiler" /> class.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <param name="logger">The logger.</param>
- public Profiler(string name, ILogger<Profiler> logger)
- {
- this._name = name;
-
- _logger = logger;
-
- _stopwatch = new Stopwatch();
- _stopwatch.Start();
- }
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- 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 (dispose)
- {
- _stopwatch.Stop();
- string message;
- if (_stopwatch.ElapsedMilliseconds > 300000)
- {
- message = string.Format(
- CultureInfo.InvariantCulture,
- "{0} took {1} minutes.",
- _name,
- ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F", CultureInfo.InvariantCulture));
- }
- else
- {
- message = string.Format(
- CultureInfo.InvariantCulture,
- "{0} took {1} seconds.",
- _name,
- ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000", CultureInfo.InvariantCulture));
- }
-
- _logger.LogInformation(message);
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Library/SearchHintInfo.cs b/MediaBrowser.Controller/Library/SearchHintInfo.cs
index 897c2b7f4..de7806adc 100644
--- a/MediaBrowser.Controller/Library/SearchHintInfo.cs
+++ b/MediaBrowser.Controller/Library/SearchHintInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Library
diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs
index a3aa6019e..968338dc6 100644
--- a/MediaBrowser.Controller/Library/TVUtils.cs
+++ b/MediaBrowser.Controller/Library/TVUtils.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
namespace MediaBrowser.Controller.Library
{
@@ -12,7 +13,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="day">The day.</param>
/// <returns>List{DayOfWeek}.</returns>
- public static DayOfWeek[] GetAirDays(string day)
+ [return: NotNullIfNotNull("day")]
+ public static DayOfWeek[]? GetAirDays(string? day)
{
if (!string.IsNullOrEmpty(day))
{
diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
index cd9109753..bfe433c97 100644
--- a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
+++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs b/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs
new file mode 100644
index 000000000..463061e68
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs
@@ -0,0 +1,19 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Threading;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class ActiveRecordingInfo
+ {
+ public string Id { get; set; }
+
+ public string Path { get; set; }
+
+ public TimerInfo Timer { get; set; }
+
+ public CancellationTokenSource CancellationTokenSource { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
index 44bd38b54..699c15f93 100644
--- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.LiveTv;
@@ -22,7 +24,7 @@ namespace MediaBrowser.Controller.LiveTv
public string Number { get; set; }
/// <summary>
- /// Get or sets the Id.
+ /// Gets or sets the Id.
/// </summary>
/// <value>The id of the channel.</value>
public string Id { get; set; }
@@ -46,13 +48,19 @@ namespace MediaBrowser.Controller.LiveTv
public ChannelType ChannelType { get; set; }
/// <summary>
- /// Supply the image path if it can be accessed directly from the file system.
+ /// Gets or sets the group of the channel.
+ /// </summary>
+ /// <value>The group of the channel.</value>
+ public string ChannelGroup { get; set; }
+
+ /// <summary>
+ /// Gets or sets the the image path if it can be accessed directly from the file system.
/// </summary>
/// <value>The image path.</value>
public string ImagePath { get; set; }
/// <summary>
- /// Supply the image url if it can be downloaded.
+ /// Gets or sets the image url if it can be downloaded.
/// </summary>
/// <value>The image URL.</value>
public string ImageUrl { get; set; }
diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs
index 038ff2eae..2bd4b20e8 100644
--- a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs
+++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index 54495c1c4..f4dc18e11 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -266,16 +268,21 @@ namespace MediaBrowser.Controller.LiveTv
void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user);
Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
+
Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
IListingsProvider[] ListingProviders { get; }
List<NameIdPair> GetTunerHostTypes();
+
Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
+
event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
+
event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
+
event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
string GetEmbyTvActiveRecordingPath(string id);
@@ -286,15 +293,4 @@ namespace MediaBrowser.Controller.LiveTv
List<BaseItem> GetRecordingFolders(User user);
}
-
- public class ActiveRecordingInfo
- {
- public string Id { get; set; }
-
- public string Path { get; set; }
-
- public TimerInfo Timer { get; set; }
-
- public CancellationTokenSource CancellationTokenSource { get; set; }
- }
}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
index 3ca1d165e..897f263f3 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
index abca8f239..7dced9f5e 100644
--- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs
+++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index ec933caf3..1a893fc2d 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -146,7 +148,7 @@ namespace MediaBrowser.Controller.LiveTv
public bool IsNews { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether this instance is kids.
+ /// Gets a value indicating whether this instance is kids.
/// </summary>
/// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
[JsonIgnore]
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 43af495dd..9d638a0bf 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -51,6 +53,7 @@ namespace MediaBrowser.Controller.LiveTv
}
private static string EmbyServiceName = "Emby";
+
public override double GetDefaultPrimaryImageAspectRatio()
{
var serviceName = ServiceName;
@@ -69,7 +72,7 @@ namespace MediaBrowser.Controller.LiveTv
public override SourceType SourceType => SourceType.LiveTV;
/// <summary>
- /// The start date of the program, in UTC.
+ /// Gets or sets start date of the program, in UTC.
/// </summary>
[JsonIgnore]
public DateTime StartDate { get; set; }
@@ -99,7 +102,7 @@ namespace MediaBrowser.Controller.LiveTv
public bool IsMovie { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether this instance is sports.
+ /// Gets a value indicating whether this instance is sports.
/// </summary>
/// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
[JsonIgnore]
@@ -113,49 +116,49 @@ namespace MediaBrowser.Controller.LiveTv
public bool IsSeries { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether this instance is live.
+ /// Gets a value indicating whether this instance is live.
/// </summary>
/// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
[JsonIgnore]
public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Gets or sets a value indicating whether this instance is news.
+ /// Gets a value indicating whether this instance is news.
/// </summary>
/// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
[JsonIgnore]
public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Gets or sets a value indicating whether this instance is kids.
+ /// Gets a value indicating whether this instance is kids.
/// </summary>
/// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
[JsonIgnore]
public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Gets or sets a value indicating whether this instance is premiere.
+ /// Gets a value indicating whether this instance is premiere.
/// </summary>
/// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
[JsonIgnore]
public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
[JsonIgnore]
public override string ContainingFolderPath => Path;
- //[JsonIgnore]
+ // [JsonIgnore]
// public override string MediaType
- //{
+ // {
// get
// {
// return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio;
// }
- //}
+ // }
[JsonIgnore]
public bool IsAiring
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
index b62974904..eb3babc18 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
index 739978e7c..aa5eb59d1 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
index f9f559ee9..3c3ac2471 100644
--- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,8 +10,16 @@ namespace MediaBrowser.Controller.LiveTv
{
public class ProgramInfo
{
+ public ProgramInfo()
+ {
+ Genres = new List<string>();
+
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
/// <summary>
- /// Id of the program.
+ /// Gets or sets the id of the program.
/// </summary>
public string Id { get; set; }
@@ -20,7 +30,7 @@ namespace MediaBrowser.Controller.LiveTv
public string ChannelId { get; set; }
/// <summary>
- /// Name of the program.
+ /// Gets or sets the name of the program.
/// </summary>
public string Name { get; set; }
@@ -43,17 +53,17 @@ namespace MediaBrowser.Controller.LiveTv
public string ShortOverview { get; set; }
/// <summary>
- /// The start date of the program, in UTC.
+ /// Gets or sets the start date of the program, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the program, in UTC.
+ /// Gets or sets the end date of the program, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
/// <summary>
- /// Genre of the program.
+ /// Gets or sets the genre of the program.
/// </summary>
public List<string> Genres { get; set; }
@@ -69,6 +79,9 @@ namespace MediaBrowser.Controller.LiveTv
/// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value>
public bool? IsHD { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is 3d.
+ /// </summary>
public bool? Is3D { get; set; }
/// <summary>
@@ -98,13 +111,13 @@ namespace MediaBrowser.Controller.LiveTv
public string EpisodeTitle { get; set; }
/// <summary>
- /// Supply the image path if it can be accessed directly from the file system.
+ /// Gets or sets the image path if it can be accessed directly from the file system.
/// </summary>
/// <value>The image path.</value>
public string ImagePath { get; set; }
/// <summary>
- /// Supply the image url if it can be downloaded.
+ /// Gets or sets the image url if it can be downloaded.
/// </summary>
/// <value>The image URL.</value>
public string ImageUrl { get; set; }
@@ -210,13 +223,5 @@ namespace MediaBrowser.Controller.LiveTv
public Dictionary<string, string> ProviderIds { get; set; }
public Dictionary<string, string> SeriesProviderIds { get; set; }
-
- public ProgramInfo()
- {
- Genres = new List<string>();
-
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
index 69190694f..1dcf7a58f 100644
--- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,8 +10,13 @@ namespace MediaBrowser.Controller.LiveTv
{
public class RecordingInfo
{
+ public RecordingInfo()
+ {
+ Genres = new List<string>();
+ }
+
/// <summary>
- /// Id of the recording.
+ /// Gets or sets the id of the recording.
/// </summary>
public string Id { get; set; }
@@ -26,7 +33,7 @@ namespace MediaBrowser.Controller.LiveTv
public string TimerId { get; set; }
/// <summary>
- /// ChannelId of the recording.
+ /// Gets or sets the channelId of the recording.
/// </summary>
public string ChannelId { get; set; }
@@ -37,7 +44,7 @@ namespace MediaBrowser.Controller.LiveTv
public ChannelType ChannelType { get; set; }
/// <summary>
- /// Name of the recording.
+ /// Gets or sets the name of the recording.
/// </summary>
public string Name { get; set; }
@@ -60,12 +67,12 @@ namespace MediaBrowser.Controller.LiveTv
public string Overview { get; set; }
/// <summary>
- /// The start date of the recording, in UTC.
+ /// Gets or sets the start date of the recording, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the recording, in UTC.
+ /// Gets or sets the end date of the recording, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
@@ -82,7 +89,7 @@ namespace MediaBrowser.Controller.LiveTv
public RecordingStatus Status { get; set; }
/// <summary>
- /// Genre of the program.
+ /// Gets or sets the genre of the program.
/// </summary>
public List<string> Genres { get; set; }
@@ -171,13 +178,13 @@ namespace MediaBrowser.Controller.LiveTv
public float? CommunityRating { get; set; }
/// <summary>
- /// Supply the image path if it can be accessed directly from the file system.
+ /// Gets or sets the image path if it can be accessed directly from the file system.
/// </summary>
/// <value>The image path.</value>
public string ImagePath { get; set; }
/// <summary>
- /// Supply the image url if it can be downloaded.
+ /// Gets or sets the image url if it can be downloaded.
/// </summary>
/// <value>The image URL.</value>
public string ImageUrl { get; set; }
@@ -199,10 +206,5 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The date last updated.</value>
public DateTime DateLastUpdated { get; set; }
-
- public RecordingInfo()
- {
- Genres = new List<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
index 847c0ea8c..0b943c939 100644
--- a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
+++ b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
index 1343ecd98..d6811fe14 100644
--- a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,13 +10,20 @@ namespace MediaBrowser.Controller.LiveTv
{
public class SeriesTimerInfo
{
+ public SeriesTimerInfo()
+ {
+ Days = new List<DayOfWeek>();
+ SkipEpisodesInLibrary = true;
+ KeepUntil = KeepUntil.UntilDeleted;
+ }
+
/// <summary>
- /// Id of the recording.
+ /// Gets or sets the id of the recording.
/// </summary>
public string Id { get; set; }
/// <summary>
- /// ChannelId of the recording.
+ /// Gets or sets the channelId of the recording.
/// </summary>
public string ChannelId { get; set; }
@@ -25,24 +34,27 @@ namespace MediaBrowser.Controller.LiveTv
public string ProgramId { get; set; }
/// <summary>
- /// Name of the recording.
+ /// Gets or sets the name of the recording.
/// </summary>
public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the service name.
+ /// </summary>
public string ServiceName { get; set; }
/// <summary>
- /// Description of the recording.
+ /// Gets or sets the description of the recording.
/// </summary>
public string Overview { get; set; }
/// <summary>
- /// The start date of the recording, in UTC.
+ /// Gets or sets the start date of the recording, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the recording, in UTC.
+ /// Gets or sets the end date of the recording, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
@@ -111,12 +123,5 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The series identifier.</value>
public string SeriesId { get; set; }
-
- public SeriesTimerInfo()
- {
- Days = new List<DayOfWeek>();
- SkipEpisodesInLibrary = true;
- KeepUntil = KeepUntil.UntilDeleted;
- }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
index 1b8f41db6..92eb0be9c 100644
--- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
index aa5170617..1a2e8acb3 100644
--- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -26,18 +28,17 @@ namespace MediaBrowser.Controller.LiveTv
public string[] Tags { get; set; }
/// <summary>
- /// Id of the recording.
+ /// Gets or sets the id of the recording.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the series timer identifier.
/// </summary>
- /// <value>The series timer identifier.</value>
public string SeriesTimerId { get; set; }
/// <summary>
- /// ChannelId of the recording.
+ /// Gets or sets the channelId of the recording.
/// </summary>
public string ChannelId { get; set; }
@@ -50,24 +51,24 @@ namespace MediaBrowser.Controller.LiveTv
public string ShowId { get; set; }
/// <summary>
- /// Name of the recording.
+ /// Gets or sets the name of the recording.
/// </summary>
public string Name { get; set; }
/// <summary>
- /// Description of the recording.
+ /// Gets or sets the description of the recording.
/// </summary>
public string Overview { get; set; }
public string SeriesId { get; set; }
/// <summary>
- /// The start date of the recording, in UTC.
+ /// Gets or sets the start date of the recording, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the recording, in UTC.
+ /// Gets or sets the end date of the recording, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
@@ -131,7 +132,7 @@ namespace MediaBrowser.Controller.LiveTv
public bool IsSeries { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether this instance is live.
+ /// Gets a value indicating whether this instance is live.
/// </summary>
/// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
[JsonIgnore]
diff --git a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
index 2759b314f..1c1a4417d 100644
--- a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
+++ b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.LiveTv
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 6b1c096ac..ee76ff080 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -16,13 +16,14 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" />
+ <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
+ <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
</ItemGroup>
<ItemGroup>
@@ -34,6 +35,9 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
@@ -47,14 +51,9 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 1f3abe8f4..745ee6bdb 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -1,61 +1,13 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.Linq;
using MediaBrowser.Model.Dlna;
namespace MediaBrowser.Controller.MediaEncoding
{
- public class EncodingJobOptions : BaseEncodingJobOptions
- {
- public string OutputDirectory { get; set; }
-
- public string ItemId { get; set; }
-
- public string TempDirectory { get; set; }
-
- public bool ReadInputAtNativeFramerate { get; set; }
-
- /// <summary>
- /// Gets a value indicating whether this instance has fixed resolution.
- /// </summary>
- /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
- public bool HasFixedResolution => Width.HasValue || Height.HasValue;
-
- public DeviceProfile DeviceProfile { get; set; }
-
- public EncodingJobOptions(StreamInfo info, DeviceProfile deviceProfile)
- {
- Container = info.Container;
- StartTimeTicks = info.StartPositionTicks;
- MaxWidth = info.MaxWidth;
- MaxHeight = info.MaxHeight;
- MaxFramerate = info.MaxFramerate;
- Id = info.ItemId;
- MediaSourceId = info.MediaSourceId;
- AudioCodec = info.TargetAudioCodec.FirstOrDefault();
- MaxAudioChannels = info.GlobalMaxAudioChannels;
- AudioBitRate = info.AudioBitrate;
- AudioSampleRate = info.TargetAudioSampleRate;
- DeviceProfile = deviceProfile;
- VideoCodec = info.TargetVideoCodec.FirstOrDefault();
- VideoBitRate = info.VideoBitrate;
- AudioStreamIndex = info.AudioStreamIndex;
- SubtitleMethod = info.SubtitleDeliveryMethod;
- Context = info.Context;
- TranscodingMaxAudioChannels = info.TranscodingMaxAudioChannels;
-
- if (info.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External)
- {
- SubtitleStreamIndex = info.SubtitleStreamIndex;
- }
-
- StreamOptions = info.StreamOptions;
- }
- }
-
- // For now until api and media encoding layers are unified
public class BaseEncodingJobOptions
{
/// <summary>
@@ -249,4 +201,4 @@ namespace MediaBrowser.Controller.MediaEncoding
StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
-}
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 07a9f5ba6..26b0bc3de 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,8 +12,6 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
@@ -27,9 +27,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private static readonly string[] _videoProfiles = new[]
{
@@ -44,14 +42,10 @@ namespace MediaBrowser.Controller.MediaEncoding
public EncodingHelper(
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration)
+ ISubtitleEncoder subtitleEncoder)
{
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
_subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
}
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
@@ -131,6 +125,12 @@ namespace MediaBrowser.Controller.MediaEncoding
private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
{
var videoStream = state.VideoStream;
+ if (videoStream == null)
+ {
+ // Remote stream doesn't have media info, disable vpp tonemapping.
+ return false;
+ }
+
var codec = videoStream.Codec;
if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
{
@@ -307,6 +307,12 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
+ // ISO files don't have an ffmpeg format
+ if (string.Equals(container, "iso", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
return container;
}
@@ -535,6 +541,8 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append(encodingOptions.VaapiDevice)
.Append(' ');
}
+
+ arg.Append("-autorotate 0 ");
}
if (state.IsVideoRequest
@@ -579,6 +587,8 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append("-init_hw_device qsv@va ")
.Append("-hwaccel_output_format vaapi ");
}
+
+ arg.Append("-autorotate 0 ");
}
}
@@ -586,13 +596,14 @@ namespace MediaBrowser.Controller.MediaEncoding
&& string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
&& isNvdecDecoder)
{
- arg.Append("-hwaccel_output_format cuda ");
+ // Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562
+ arg.Append("-hwaccel_output_format cuda -extra_hw_frames 3 -autorotate 0 ");
}
if (state.IsVideoRequest
&& ((string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
&& (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder))
- || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)
+ || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)
&& (isD3d11vaDecoder || isSwDecoder))))
{
if (isTonemappingSupported)
@@ -1060,7 +1071,6 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
{
- // following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead.
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
@@ -1156,7 +1166,9 @@ namespace MediaBrowser.Controller.MediaEncoding
profileScore = Math.Min(profileScore, 2);
// http://www.webmproject.org/docs/encoder-parameters/
- param += string.Format(CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
+ param += string.Format(
+ CultureInfo.InvariantCulture,
+ " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
profileScore.ToString(_usCulture),
crf,
qmin,
@@ -1241,7 +1253,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
- && profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase))
+ && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
{
profile = "constrained_baseline";
}
@@ -1286,7 +1298,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hevc_qsv use -level 51 instead of -level 153.
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
{
- param += " -level " + hevcLevel / 3;
+ param += " -level " + (hevcLevel / 3);
}
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
@@ -1381,7 +1393,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedProfile = requestedProfiles[0];
// strip spaces because they may be stripped out on the query string as well
- if (!string.IsNullOrEmpty(videoStream.Profile) && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", ""), StringComparer.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(videoStream.Profile)
+ && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", string.Empty, StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
{
var currentScore = GetVideoProfileScore(videoStream.Profile);
var requestedScore = GetVideoProfileScore(requestedProfile);
@@ -1710,7 +1723,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (filters.Count > 0)
{
- return " -af \"" + string.Join(",", filters) + "\"";
+ return " -af \"" + string.Join(',', filters) + "\"";
}
return string.Empty;
@@ -1790,7 +1803,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (isTranscodingAudio
&& state.TranscodingType != TranscodingJobType.Progressive
&& resultChannels.HasValue
- && (resultChannels.Value > 2 && resultChannels.Value < 6 || resultChannels.Value == 7))
+ && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7))
{
resultChannels = 2;
}
@@ -2118,8 +2131,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return (null, null);
}
- decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth);
- decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight);
+ decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth, CultureInfo.InvariantCulture);
+ decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight, CultureInfo.InvariantCulture);
decimal outputWidth = requestedWidth.HasValue ? Convert.ToDecimal(requestedWidth.Value) : inputWidth;
decimal outputHeight = requestedHeight.HasValue ? Convert.ToDecimal(requestedHeight.Value) : inputHeight;
decimal maximumWidth = requestedMaxWidth.HasValue ? Convert.ToDecimal(requestedMaxWidth.Value) : outputWidth;
@@ -2186,12 +2199,11 @@ namespace MediaBrowser.Controller.MediaEncoding
var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
var isTonemappingSupported = IsTonemappingSupported(state, options);
var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
- var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)&& isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
+ var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported))
|| (isTonemappingSupportedOnQsv && isVppTonemappingSupported);
-
var outputPixFmt = "format=nv12";
if (isP010PixFmtRequired)
{
@@ -2530,7 +2542,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
// If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices
- var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30;
+ var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.AverageFrameRate ?? 60) <= 30;
var isScalingInAdvance = false;
var isCudaDeintInAdvance = false;
@@ -2888,7 +2900,7 @@ namespace MediaBrowser.Controller.MediaEncoding
output += string.Format(
CultureInfo.InvariantCulture,
"{0}",
- string.Join(",", filters));
+ string.Join(',', filters));
}
return output;
@@ -2914,7 +2926,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (threads <= 0)
{
return 0;
- }
+ }
else if (threads >= Environment.ProcessorCount)
{
return Environment.ProcessorCount;
@@ -2922,6 +2934,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return threads;
}
+
#nullable disable
public void TryStreamCopy(EncodingJobInfo state)
{
@@ -3080,7 +3093,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
inputModifier += " -deint 1";
- if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.RealFrameRate ?? 60) > 30)
+ if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.AverageFrameRate ?? 60) > 30)
{
inputModifier += " -drop_second_field 1";
}
@@ -3163,8 +3176,8 @@ namespace MediaBrowser.Controller.MediaEncoding
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
if (state.ReadInputAtNativeFramerate
- || mediaSource.Protocol == MediaProtocol.File
- && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
+ || (mediaSource.Protocol == MediaProtocol.File
+ && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)))
{
state.InputVideoSync = "-1";
state.InputAudioSync = "1";
@@ -3537,7 +3550,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets a hw decoder name
+ /// Gets a hw decoder name.
/// </summary>
public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10)
{
@@ -3555,7 +3568,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system
+ /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system.
/// </summary>
public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10)
{
@@ -3681,7 +3694,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (flags.Count > 0)
{
- return " -fflags " + string.Join("", flags);
+ return " -fflags " + string.Join(string.Empty, flags);
}
return string.Empty;
@@ -3864,7 +3877,7 @@ namespace MediaBrowser.Controller.MediaEncoding
GetInputArgument(state, encodingOptions),
threads,
" -vn",
- string.Join(" ", audioTranscodeParams),
+ string.Join(' ', audioTranscodeParams),
outputPath,
string.Empty,
string.Empty,
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index dacd6dea6..bc0318ad7 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -9,7 +11,6 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
@@ -68,6 +69,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
private TranscodeReason[] _transcodeReasons = null;
+
public TranscodeReason[] TranscodeReasons
{
get
@@ -273,6 +275,16 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetRequestedAudioChannels(string codec)
{
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "audiochannels");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
if (BaseRequest.MaxAudioChannels.HasValue)
{
return BaseRequest.MaxAudioChannels;
@@ -288,16 +300,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return BaseRequest.TranscodingMaxAudioChannels;
}
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "audiochannels");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
return null;
}
@@ -429,7 +431,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target video level.
/// </summary>
public double? TargetVideoLevel
{
@@ -452,7 +454,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target video bit depth.
/// </summary>
public int? TargetVideoBitDepth
{
@@ -487,7 +489,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target framerate.
/// </summary>
public float? TargetFramerate
{
@@ -519,7 +521,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target packet length.
/// </summary>
public int? TargetPacketLength
{
@@ -535,7 +537,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target video profile.
/// </summary>
public string TargetVideoProfile
{
@@ -699,25 +701,4 @@ namespace MediaBrowser.Controller.MediaEncoding
Progress.Report(percentComplete.Value);
}
}
-
- /// <summary>
- /// Enum TranscodingJobType.
- /// </summary>
- public enum TranscodingJobType
- {
- /// <summary>
- /// The progressive.
- /// </summary>
- Progressive,
-
- /// <summary>
- /// The HLS.
- /// </summary>
- Hls,
-
- /// <summary>
- /// The dash.
- /// </summary>
- Dash
- }
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
index fbc827534..c38e7ec3b 100644
--- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
index 15a2580af..773547872 100644
--- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 5cbb57990..76a9fd7c7 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,7 +9,6 @@ using System.Threading.Tasks;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.System;
@@ -19,7 +20,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public interface IMediaEncoder : ITranscoderSupport
{
/// <summary>
- /// The location of the discovered FFmpeg tool.
+ /// Gets location of the discovered FFmpeg tool.
/// </summary>
FFmpegLocation EncoderLocation { get; }
diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
index 6ebf7f159..3fb2c47e1 100644
--- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
index e7b4c8c15..044ba6d33 100644
--- a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.MediaEncoding
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index cc8820f39..b23c95112 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -1,10 +1,12 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
index 281d50372..841e7b287 100644
--- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
+++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
@@ -1,11 +1,5 @@
#pragma warning disable CS1591
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using MediaBrowser.Model.IO;
-
namespace MediaBrowser.Controller.MediaEncoding
{
/// <summary>
diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
index 2cb04bdc4..1dd8bcf31 100644
--- a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
+++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.Dlna;
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs
new file mode 100644
index 000000000..66b628371
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ /// <summary>
+ /// Enum TranscodingJobType.
+ /// </summary>
+ public enum TranscodingJobType
+ {
+ /// <summary>
+ /// The progressive.
+ /// </summary>
+ Progressive,
+
+ /// <summary>
+ /// The HLS.
+ /// </summary>
+ Hls,
+
+ /// <summary>
+ /// The dash.
+ /// </summary>
+ Dash
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
index 93573e08e..2452b25ab 100644
--- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs
+++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using Jellyfin.Data.Entities;
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 163a9c8f8..d8995ce74 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -268,13 +270,4 @@ namespace MediaBrowser.Controller.Net
GC.SuppressFinalize(this);
}
}
-
- public class WebSocketListenerState
- {
- public DateTime DateLastSendUtc { get; set; }
-
- public long InitialDelayMs { get; set; }
-
- public long IntervalMs { get; set; }
- }
}
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
index 04b2e13e8..d15c6d318 100644
--- a/MediaBrowser.Controller/Net/IAuthService.cs
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs
index a60dc2ea1..6b896b41f 100644
--- a/MediaBrowser.Controller/Net/ISessionContext.cs
+++ b/MediaBrowser.Controller/Net/ISessionContext.cs
@@ -10,10 +10,10 @@ namespace MediaBrowser.Controller.Net
{
SessionInfo GetSession(object requestContext);
- User GetUser(object requestContext);
+ User? GetUser(object requestContext);
SessionInfo GetSession(HttpContext requestContext);
- User GetUser(HttpContext requestContext);
+ User? GetUser(HttpContext requestContext);
}
}
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index e87f3bca6..c8c5caf80 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -1,7 +1,5 @@
#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Net;
using System.Net.WebSockets;
@@ -32,7 +30,7 @@ namespace MediaBrowser.Controller.Net
DateTime LastKeepAliveDate { get; set; }
/// <summary>
- /// Gets or sets the query string.
+ /// Gets the query string.
/// </summary>
/// <value>The query string.</value>
IQueryCollection QueryString { get; }
@@ -58,11 +56,11 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// Sends a message asynchronously.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of websocket message data.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">message</exception>
+ /// <exception cref="ArgumentNullException">The message is null.</exception>
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
Task ProcessAsync(CancellationToken cancellationToken = default);
diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs
index c6347133a..f0d0b45a0 100644
--- a/MediaBrowser.Controller/Net/SecurityException.cs
+++ b/MediaBrowser.Controller/Net/SecurityException.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
namespace MediaBrowser.Controller.Net
diff --git a/MediaBrowser.Controller/Net/WebSocketListenerState.cs b/MediaBrowser.Controller/Net/WebSocketListenerState.cs
new file mode 100644
index 000000000..70604d60a
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketListenerState.cs
@@ -0,0 +1,17 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Controller.Net
+{
+ public class WebSocketListenerState
+ {
+ public DateTime DateLastSendUtc { get; set; }
+
+ public long InitialDelayMs { get; set; }
+
+ public long IntervalMs { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
index be0b3ddc3..6f7ebf156 100644
--- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
+++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Model.Net;
namespace MediaBrowser.Controller.Net
diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs
index 08d9bc12a..7caba1097 100644
--- a/MediaBrowser.Controller/Notifications/INotificationManager.cs
+++ b/MediaBrowser.Controller/Notifications/INotificationManager.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Controller.Notifications
/// <returns>Task.</returns>
Task SendNotification(NotificationRequest request, CancellationToken cancellationToken);
- Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken);
+ Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken);
/// <summary>
/// Adds the parts.
diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs
index fa947220a..535c08795 100644
--- a/MediaBrowser.Controller/Notifications/INotificationService.cs
+++ b/MediaBrowser.Controller/Notifications/INotificationService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Threading;
diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs
index d768abfe7..4be0e09ae 100644
--- a/MediaBrowser.Controller/Notifications/UserNotification.cs
+++ b/MediaBrowser.Controller/Notifications/UserNotification.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 45c6805f0..0a9073e7f 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -47,21 +49,23 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Gets chapters for an item.
/// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
+ /// <param name="id">The item.</param>
+ /// <returns>The list of chapter info.</returns>
List<ChapterInfo> GetChapters(BaseItem id);
/// <summary>
/// Gets a single chapter for an item.
/// </summary>
- /// <param name="id"></param>
- /// <param name="index"></param>
- /// <returns></returns>
+ /// <param name="id">The item.</param>
+ /// <param name="index">The chapter index.</param>
+ /// <returns>The chapter info at the specified index.</returns>
ChapterInfo GetChapter(BaseItem id, int index);
/// <summary>
/// Saves the chapters.
/// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="chapters">The list of chapters to save.</param>
void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters);
/// <summary>
@@ -153,8 +157,7 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Updates the inherited values.
/// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- void UpdateInheritedValues(CancellationToken cancellationToken);
+ void UpdateInheritedValues();
int GetCount(InternalItemsQuery query);
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
index 81ba513ce..5fa5834c8 100644
--- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Controller.Entities;
@@ -38,17 +40,16 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Return all user data associated with the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <returns>The list of user item data.</returns>
List<UserItemData> GetAllUserData(long userId);
/// <summary>
/// Save all user data associated with the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <param name="userData"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userData">The user item data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index e8b7be7e2..3eaf23515 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -1,8 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
@@ -13,21 +16,20 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Playlists
{
public class Playlist : Folder, IHasShares
{
- public static string[] SupportedExtensions =
- {
- ".m3u",
- ".m3u8",
- ".pls",
- ".wpl",
- ".zpl"
- };
+ public static readonly IReadOnlyList<string> SupportedExtensions = new[]
+ {
+ ".m3u",
+ ".m3u8",
+ ".pls",
+ ".wpl",
+ ".zpl"
+ };
public Guid OwnerUserId { get; set; }
@@ -43,7 +45,8 @@ namespace MediaBrowser.Controller.Playlists
public static bool IsPlaylistFile(string path)
{
- return System.IO.Path.HasExtension(path);
+ // The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot).
+ return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
}
[JsonIgnore]
@@ -98,7 +101,7 @@ namespace MediaBrowser.Controller.Playlists
return new List<BaseItem>();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
@@ -125,10 +128,7 @@ namespace MediaBrowser.Controller.Playlists
private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query)
{
- if (query == null)
- {
- query = new InternalItemsQuery(user);
- }
+ query ??= new InternalItemsQuery(user);
query.IsFolder = false;
@@ -162,7 +162,7 @@ namespace MediaBrowser.Controller.Playlists
Recursive = true,
IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = new[] { musicGenre.Id },
- OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
});
}
@@ -174,7 +174,7 @@ namespace MediaBrowser.Controller.Playlists
Recursive = true,
IncludeItemTypes = new[] { nameof(Audio) },
ArtistIds = new[] { musicArtist.Id },
- OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
});
}
diff --git a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs
deleted file mode 100644
index bf15fe040..000000000
--- a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-using System.Reflection;
-
-namespace MediaBrowser.Controller.Plugins
-{
- public interface ILocalizablePlugin
- {
- Stream GetDictionary(string culture);
- }
-
- public static class LocalizablePluginHelper
- {
- public static Stream GetDictionary(Assembly assembly, string manifestPrefix, string culture)
- {
- // Find all dictionaries using GetManifestResourceNames, start start with the prefix
- // Return the one for the culture if exists, otherwise return the default
- return null;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
new file mode 100644
index 000000000..2b831103a
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Plugins
+{
+ /// <summary>
+ /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task.
+ /// </summary>
+ public interface IRunBeforeStartup
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
index b44e2531e..6024661e1 100644
--- a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
+++ b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
@@ -14,13 +14,7 @@ namespace MediaBrowser.Controller.Plugins
/// <summary>
/// Run the initialization for this module. This method is invoked at application start.
/// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task RunAsync();
}
-
- /// <summary>
- /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task.
- /// </summary>
- public interface IRunBeforeStartup
- {
- }
}
diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs
index 276bcf125..c7fad5974 100644
--- a/MediaBrowser.Controller/Providers/AlbumInfo.cs
+++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs
@@ -7,6 +7,13 @@ namespace MediaBrowser.Controller.Providers
{
public class AlbumInfo : ItemLookupInfo
{
+ public AlbumInfo()
+ {
+ ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SongInfos = new List<SongInfo>();
+ AlbumArtists = Array.Empty<string>();
+ }
+
/// <summary>
/// Gets or sets the album artist.
/// </summary>
@@ -20,12 +27,5 @@ namespace MediaBrowser.Controller.Providers
public Dictionary<string, string> ArtistProviderIds { get; set; }
public List<SongInfo> SongInfos { get; set; }
-
- public AlbumInfo()
- {
- ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- SongInfos = new List<SongInfo>();
- AlbumArtists = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/Providers/ArtistInfo.cs b/MediaBrowser.Controller/Providers/ArtistInfo.cs
index adf885baa..e9181f476 100644
--- a/MediaBrowser.Controller/Providers/ArtistInfo.cs
+++ b/MediaBrowser.Controller/Providers/ArtistInfo.cs
@@ -6,11 +6,11 @@ namespace MediaBrowser.Controller.Providers
{
public class ArtistInfo : ItemLookupInfo
{
- public List<SongInfo> SongInfos { get; set; }
-
public ArtistInfo()
{
SongInfos = new List<SongInfo>();
}
+
+ public List<SongInfo> SongInfos { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Providers/BookInfo.cs b/MediaBrowser.Controller/Providers/BookInfo.cs
index cce0a25fc..3055c5d87 100644
--- a/MediaBrowser.Controller/Providers/BookInfo.cs
+++ b/MediaBrowser.Controller/Providers/BookInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 16fd1d42b..b31270270 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -12,11 +12,11 @@ namespace MediaBrowser.Controller.Providers
{
private readonly IFileSystem _fileSystem;
- private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new ConcurrentDictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new (StringComparer.Ordinal);
- private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new ConcurrentDictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new (StringComparer.Ordinal);
- private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new ConcurrentDictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new (StringComparer.Ordinal);
public DirectoryService(IFileSystem fileSystem)
{
@@ -25,15 +25,16 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
- return _cache.GetOrAdd(path, p => _fileSystem.GetFileSystemEntries(p).ToArray());
+ return _cache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
}
public List<FileSystemMetadata> GetFiles(string path)
{
var list = new List<FileSystemMetadata>();
var items = GetFileSystemEntries(path);
- foreach (var item in items)
+ for (var i = 0; i < items.Length; i++)
{
+ var item = items[i];
if (!item.IsDirectory)
{
list.Add(item);
@@ -43,18 +44,16 @@ namespace MediaBrowser.Controller.Providers
return list;
}
- public FileSystemMetadata GetFile(string path)
+ public FileSystemMetadata? GetFile(string path)
{
- var result = _fileCache.GetOrAdd(path, p =>
+ if (!_fileCache.TryGetValue(path, out var result))
{
- var file = _fileSystem.GetFileInfo(p);
- return file != null && file.Exists ? file : null;
- });
-
- if (result == null)
- {
- // lets not store null results in the cache
- _fileCache.TryRemove(path, out _);
+ var file = _fileSystem.GetFileInfo(path);
+ if (file.Exists)
+ {
+ result = file;
+ _fileCache.TryAdd(path, result);
+ }
}
return result;
@@ -63,14 +62,21 @@ namespace MediaBrowser.Controller.Providers
public IReadOnlyList<string> GetFilePaths(string path)
=> GetFilePaths(path, false);
- public IReadOnlyList<string> GetFilePaths(string path, bool clearCache)
+ public IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false)
{
if (clearCache)
{
_filePathCache.TryRemove(path, out _);
}
- return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(p).ToList());
+ var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem);
+
+ if (sort)
+ {
+ filePaths.Sort();
+ }
+
+ return filePaths;
}
}
}
diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs
index 006174be8..66fddb0e3 100644
--- a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs
+++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
index a4c8dab7e..0c932fa87 100644
--- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs
+++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,11 @@ namespace MediaBrowser.Controller.Providers
{
public class EpisodeInfo : ItemLookupInfo
{
+ public EpisodeInfo()
+ {
+ SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
public Dictionary<string, string> SeriesProviderIds { get; set; }
public int? IndexNumberEnd { get; set; }
@@ -14,10 +21,5 @@ namespace MediaBrowser.Controller.Providers
public bool IsMissingEpisode { get; set; }
public string SeriesDisplayOrder { get; set; }
-
- public EpisodeInfo()
- {
- SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index f06481c7a..b1a36e102 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -11,10 +11,10 @@ namespace MediaBrowser.Controller.Providers
List<FileSystemMetadata> GetFiles(string path);
- FileSystemMetadata GetFile(string path);
+ FileSystemMetadata? GetFile(string path);
IReadOnlyList<string> GetFilePaths(string path);
- IReadOnlyList<string> GetFilePaths(string path, bool clearCache);
+ IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
}
}
diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs
index 5e38446bc..e2dbef2bc 100644
--- a/MediaBrowser.Controller/Providers/IExternalId.cs
+++ b/MediaBrowser.Controller/Providers/IExternalId.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs
index c129eddb3..f78bd6ddf 100644
--- a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs
+++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs
@@ -10,6 +10,6 @@ namespace MediaBrowser.Controller.Providers
/// </summary>
public interface ILocalImageProvider : IImageProvider
{
- List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService);
+ IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService);
}
}
diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs
index 5f3d4274e..05fbb18ee 100644
--- a/MediaBrowser.Controller/Providers/IMetadataService.cs
+++ b/MediaBrowser.Controller/Providers/IMetadataService.cs
@@ -11,6 +11,12 @@ namespace MediaBrowser.Controller.Providers
public interface IMetadataService
{
/// <summary>
+ /// Gets the order.
+ /// </summary>
+ /// <value>The order.</value>
+ int Order { get; }
+
+ /// <summary>
/// Determines whether this instance can refresh the specified item.
/// </summary>
/// <param name="item">The item.</param>
@@ -27,11 +33,5 @@ namespace MediaBrowser.Controller.Providers
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken);
-
- /// <summary>
- /// Gets the order.
- /// </summary>
- /// <value>The order.</value>
- int Order { get; }
}
}
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 2f5b1d4a3..684bd9e68 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -91,8 +93,11 @@ namespace MediaBrowser.Controller.Providers
/// <summary>
/// Adds the metadata providers.
/// </summary>
- void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders,
- IEnumerable<IMetadataSaver> savers,
+ void AddParts(
+ IEnumerable<IImageProvider> imageProviders,
+ IEnumerable<IMetadataService> metadataServices,
+ IEnumerable<IMetadataProvider> metadataProviders,
+ IEnumerable<IMetadataSaver> metadataSavers,
IEnumerable<IExternalId> externalIds);
/// <summary>
@@ -186,11 +191,4 @@ namespace MediaBrowser.Controller.Providers
double? GetRefreshProgress(Guid id);
}
-
- public enum RefreshPriority
- {
- High = 0,
- Normal = 1,
- Low = 2
- }
}
diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
index ee8f5b860..de1631dcf 100644
--- a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
+++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
@@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
diff --git a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
index 9592baa7c..e401ed211 100644
--- a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
+++ b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
@@ -3,7 +3,6 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
namespace MediaBrowser.Controller.Providers
{
diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
index 9fc379f04..81a22affb 100644
--- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs
index b50def043..b8dd416a2 100644
--- a/MediaBrowser.Controller/Providers/ItemInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -14,8 +16,7 @@ namespace MediaBrowser.Controller.Providers
ContainingFolderPath = item.ContainingFolderPath;
IsInMixedFolder = item.IsInMixedFolder;
- var video = item as Video;
- if (video != null)
+ if (item is Video video)
{
VideoType = video.VideoType;
IsPlaceHolder = video.IsPlaceHolder;
diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
index b777cc1d3..2fd89e3bb 100644
--- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,6 +10,12 @@ namespace MediaBrowser.Controller.Providers
{
public class ItemLookupInfo : IHasProviderIds
{
+ public ItemLookupInfo()
+ {
+ IsAutomated = true;
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
/// <summary>
/// Gets or sets the name.
/// </summary>
@@ -15,6 +23,12 @@ namespace MediaBrowser.Controller.Providers
public string Name { get; set; }
/// <summary>
+ /// Gets or sets the original title
+ /// </summary>
+ /// <value>The original title of the item.</value>
+ public string OriginalTitle { get; set; }
+
+ /// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
@@ -51,11 +65,5 @@ namespace MediaBrowser.Controller.Providers
public DateTime? PremiereDate { get; set; }
public bool IsAutomated { get; set; }
-
- public ItemLookupInfo()
- {
- IsAutomated = true;
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs
index 41801862f..a8e70e6d0 100644
--- a/MediaBrowser.Controller/Providers/LocalImageInfo.cs
+++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index b92b83701..2cf536779 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -9,21 +11,6 @@ namespace MediaBrowser.Controller.Providers
{
public class MetadataRefreshOptions : ImageRefreshOptions
{
- /// <summary>
- /// When paired with MetadataRefreshMode=FullRefresh, all existing data will be overwritten with new data from the providers.
- /// </summary>
- public bool ReplaceAllMetadata { get; set; }
-
- public MetadataRefreshMode MetadataRefreshMode { get; set; }
-
- public RemoteSearchResult SearchResult { get; set; }
-
- public string[] RefreshPaths { get; set; }
-
- public bool ForceSave { get; set; }
-
- public bool EnableRemoteContentProbe { get; set; }
-
public MetadataRefreshOptions(IDirectoryService directoryService)
: base(directoryService)
{
@@ -45,15 +32,28 @@ namespace MediaBrowser.Controller.Providers
if (copy.RefreshPaths != null && copy.RefreshPaths.Length > 0)
{
- if (RefreshPaths == null)
- {
- RefreshPaths = Array.Empty<string>();
- }
+ RefreshPaths ??= Array.Empty<string>();
RefreshPaths = copy.RefreshPaths.ToArray();
}
}
+ /// <summary>
+ /// Gets or sets a value indicating whether all existing data should be overwritten with new data from providers
+ /// when paired with MetadataRefreshMode=FullRefresh.
+ /// </summary>
+ public bool ReplaceAllMetadata { get; set; }
+
+ public MetadataRefreshMode MetadataRefreshMode { get; set; }
+
+ public RemoteSearchResult SearchResult { get; set; }
+
+ public string[] RefreshPaths { get; set; }
+
+ public bool ForceSave { get; set; }
+
+ public bool EnableRemoteContentProbe { get; set; }
+
public bool RefreshItem(BaseItem item)
{
if (RefreshPaths != null && RefreshPaths.Length > 0)
diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs
index 1c695cafa..7ec1eefcd 100644
--- a/MediaBrowser.Controller/Providers/MetadataResult.cs
+++ b/MediaBrowser.Controller/Providers/MetadataResult.cs
@@ -1,24 +1,40 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Providers
{
public class MetadataResult<T>
{
- public List<LocalImageInfo> Images { get; set; }
-
- public List<UserItemData> UserDataList { get; set; }
+ // Images aren't always used so the allocation is a waste a lot of the time
+ private List<LocalImageInfo> _images;
+ private List<(string url, ImageType type)> _remoteImages;
public MetadataResult()
{
- Images = new List<LocalImageInfo>();
ResultLanguage = "en";
}
+ public List<LocalImageInfo> Images
+ {
+ get => _images ??= new List<LocalImageInfo>();
+ set => _images = value;
+ }
+
+ public List<(string url, ImageType type)> RemoteImages
+ {
+ get => _remoteImages ??= new List<(string url, ImageType type)>();
+ set => _remoteImages = value;
+ }
+
+ public List<UserItemData> UserDataList { get; set; }
+
public List<PersonInfo> People { get; set; }
public bool HasMetadata { get; set; }
@@ -33,10 +49,7 @@ namespace MediaBrowser.Controller.Providers
public void AddPerson(PersonInfo p)
{
- if (People == null)
- {
- People = new List<PersonInfo>();
- }
+ People ??= new List<PersonInfo>();
PeopleHelper.AddPerson(People, p);
}
@@ -50,16 +63,15 @@ namespace MediaBrowser.Controller.Providers
{
People = new List<PersonInfo>();
}
-
- People.Clear();
+ else
+ {
+ People.Clear();
+ }
}
public UserItemData GetOrAddUserData(string userId)
{
- if (UserDataList == null)
- {
- UserDataList = new List<UserItemData>();
- }
+ UserDataList ??= new List<UserItemData>();
UserItemData userData = null;
diff --git a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs
index 0b927f6eb..322320abd 100644
--- a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs
+++ b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Providers/RefreshPriority.cs b/MediaBrowser.Controller/Providers/RefreshPriority.cs
new file mode 100644
index 000000000..3619f679d
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/RefreshPriority.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provider refresh priority.
+ /// </summary>
+ public enum RefreshPriority
+ {
+ /// <summary>
+ /// High priority.
+ /// </summary>
+ High = 0,
+
+ /// <summary>
+ /// Normal priority.
+ /// </summary>
+ Normal = 1,
+
+ /// <summary>
+ /// Low priority.
+ /// </summary>
+ Low = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs
index 9653bc1c4..d4df5fa0d 100644
--- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs
+++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -12,7 +14,7 @@ namespace MediaBrowser.Controller.Providers
public Guid ItemId { get; set; }
/// <summary>
- /// Will only search within the given provider when set.
+ /// Gets or sets the provider name to search within if set.
/// </summary>
public string SearchProviderName { get; set; }
diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs
index 2a4c1f03c..7e39bc37a 100644
--- a/MediaBrowser.Controller/Providers/SeasonInfo.cs
+++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs
@@ -7,11 +7,11 @@ namespace MediaBrowser.Controller.Providers
{
public class SeasonInfo : ItemLookupInfo
{
- public Dictionary<string, string> SeriesProviderIds { get; set; }
-
public SeasonInfo()
{
SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
+
+ public Dictionary<string, string> SeriesProviderIds { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs
index 58f76dca9..c90717a2e 100644
--- a/MediaBrowser.Controller/Providers/SongInfo.cs
+++ b/MediaBrowser.Controller/Providers/SongInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index 959a2d771..c4e709c24 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Model.QuickConnect;
diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
index bb80e6025..a07b3e898 100644
--- a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
+++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
@@ -4,7 +4,7 @@ using MediaBrowser.Model.IO;
namespace MediaBrowser.Controller.Resolvers
{
/// <summary>
- /// Provides a base "rule" that anyone can use to have paths ignored by the resolver
+ /// Provides a base "rule" that anyone can use to have paths ignored by the resolver.
/// </summary>
public interface IResolverIgnoreRule
{
diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index 67acdd9a3..7fd54fcc6 100644
--- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -6,27 +8,27 @@ namespace MediaBrowser.Controller.Resolvers
/// <summary>
/// Class ItemResolver.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of BaseItem.</typeparam>
public abstract class ItemResolver<T> : IItemResolver
where T : BaseItem, new()
{
/// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public virtual ResolverPriority Priority => ResolverPriority.First;
+
+ /// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- protected virtual T Resolve(ItemResolveArgs args)
+ public virtual T Resolve(ItemResolveArgs args)
{
return null;
}
/// <summary>
- /// Gets the priority.
- /// </summary>
- /// <value>The priority.</value>
- public virtual ResolverPriority Priority => ResolverPriority.First;
-
- /// <summary>
/// Sets initial values on the newly resolved item.
/// </summary>
/// <param name="item">The item.</param>
diff --git a/MediaBrowser.Controller/Security/AuthenticationInfo.cs b/MediaBrowser.Controller/Security/AuthenticationInfo.cs
index efac9273e..b4b242f1b 100644
--- a/MediaBrowser.Controller/Security/AuthenticationInfo.cs
+++ b/MediaBrowser.Controller/Security/AuthenticationInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
index c5f3da0b1..3af6a525c 100644
--- a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
+++ b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
index 883b74165..1dd69ccd8 100644
--- a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
+++ b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.Devices;
diff --git a/MediaBrowser.Controller/Session/AuthenticationRequest.cs b/MediaBrowser.Controller/Session/AuthenticationRequest.cs
index cc321fd22..647c75e66 100644
--- a/MediaBrowser.Controller/Session/AuthenticationRequest.cs
+++ b/MediaBrowser.Controller/Session/AuthenticationRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -12,6 +14,7 @@ namespace MediaBrowser.Controller.Session
public string Password { get; set; }
+ [Obsolete("Send full password in Password field")]
public string PasswordSha1 { get; set; }
public string App { get; set; }
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index bc4ccd44c..6bc39d6f4 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 6c06dcad5..4c3cf5ffe 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -344,21 +346,19 @@ namespace MediaBrowser.Controller.Session
/// Logouts the specified access token.
/// </summary>
/// <param name="accessToken">The access token.</param>
- /// <returns>Task.</returns>
void Logout(string accessToken);
+
void Logout(AuthenticationInfo accessToken);
/// <summary>
/// Revokes the user tokens.
/// </summary>
- /// <returns>Task.</returns>
void RevokeUserTokens(Guid userId, string currentAccessToken);
/// <summary>
/// Revokes the token.
/// </summary>
/// <param name="id">The identifier.</param>
- /// <returns>Task.</returns>
void RevokeToken(string id);
void CloseIfNeeded(SessionInfo session);
diff --git a/MediaBrowser.Controller/Session/SessionEventArgs.cs b/MediaBrowser.Controller/Session/SessionEventArgs.cs
index 097e32eae..269fe7dc4 100644
--- a/MediaBrowser.Controller/Session/SessionEventArgs.cs
+++ b/MediaBrowser.Controller/Session/SessionEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index d09852870..6134c0cf3 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -52,7 +54,7 @@ namespace MediaBrowser.Controller.Session
public string RemoteEndPoint { get; set; }
/// <summary>
- /// Gets or sets the playable media types.
+ /// Gets the playable media types.
/// </summary>
/// <value>The playable media types.</value>
public IReadOnlyList<string> PlayableMediaTypes
@@ -228,7 +230,7 @@ namespace MediaBrowser.Controller.Session
public string UserPrimaryImageTag { get; set; }
/// <summary>
- /// Gets or sets the supported commands.
+ /// Gets the supported commands.
/// </summary>
/// <value>The supported commands.</value>
public IReadOnlyList<GeneralCommandType> SupportedCommands
diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
index 70cb9eebe..e00cadca2 100644
--- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
+++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
@@ -1,7 +1,5 @@
#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Collections.Generic;
@@ -123,7 +121,9 @@ namespace MediaBrowser.Controller.Sorting
return result;
}
}
+#pragma warning disable SA1500 // TODO remove with StyleCop.Analyzers v1.2.0 https://github.com/DotNetAnalyzers/StyleCopAnalyzers/pull/3196
} while (pos1 < len1 && pos2 < len2);
+#pragma warning restore SA1500
return len1 - len2;
}
diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
index 727cbe639..07fe1ea8a 100644
--- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
@@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Sorting
/// <summary>
/// Interface IBaseItemComparer.
/// </summary>
- public interface IBaseItemComparer : IComparer<BaseItem>
+ public interface IBaseItemComparer : IComparer<BaseItem?>
{
/// <summary>
/// Gets the name.
diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
index 6d03d97ae..bd47db39a 100644
--- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Sorting
diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs
index 88467814c..aa6ec513f 100644
--- a/MediaBrowser.Controller/Sorting/SortExtensions.cs
+++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
index feb26bc10..9e661cbe4 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -1,8 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
index a633262de..326348d58 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs
index ce8141219..c782f5796 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
index a86b05778..85b3e6fbd 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
index 7d3c20e8f..0f7c47e76 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs
deleted file mode 100644
index e7395b136..000000000
--- a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Sync;
-
-namespace MediaBrowser.Controller.Sync
-{
- public interface IHasDynamicAccess
- {
- /// <summary>
- /// Gets the synced file information.
- /// </summary>
- /// <param name="id">The identifier.</param>
- /// <param name="target">The target.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task&lt;SyncedFileInfo&gt;.</returns>
- Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
deleted file mode 100644
index b2c53365c..000000000
--- a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace MediaBrowser.Controller.Sync
-{
- /// <summary>
- /// A marker interface.
- /// </summary>
- public interface IRemoteSyncProvider
- {
- }
-}
diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs
deleted file mode 100644
index c97fd7044..000000000
--- a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Sync;
-
-namespace MediaBrowser.Controller.Sync
-{
- public interface IServerSyncProvider : ISyncProvider
- {
- /// <summary>
- /// Transfers the file.
- /// </summary>
- Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, Stream inputStream, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
-
- Task<QueryResult<FileSystemMetadata>> GetFiles(string[] directoryPathParts, SyncTarget target, CancellationToken cancellationToken);
- }
-
- public interface ISupportsDirectCopy
- {
- /// <summary>
- /// Sends the file.
- /// </summary>
- Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, string inputPath, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/Sync/ISyncProvider.cs b/MediaBrowser.Controller/Sync/ISyncProvider.cs
deleted file mode 100644
index 950cc73e8..000000000
--- a/MediaBrowser.Controller/Sync/ISyncProvider.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.Sync;
-
-namespace MediaBrowser.Controller.Sync
-{
- public interface ISyncProvider
- {
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- string Name { get; }
-
- /// <summary>
- /// Gets the synchronize targets.
- /// </summary>
- /// <param name="userId">The user identifier.</param>
- /// <returns>IEnumerable&lt;SyncTarget&gt;.</returns>
- List<SyncTarget> GetSyncTargets(string userId);
-
- /// <summary>
- /// Gets all synchronize targets.
- /// </summary>
- /// <returns>IEnumerable&lt;SyncTarget&gt;.</returns>
- List<SyncTarget> GetAllSyncTargets();
- }
-}
diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs
deleted file mode 100644
index a626738fb..000000000
--- a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.Controller.Sync
-{
- public class SyncedFileInfo
- {
- public SyncedFileInfo()
- {
- RequiredHttpHeaders = new Dictionary<string, string>();
- }
-
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- public string Path { get; set; }
-
- public string[] PathParts { get; set; }
-
- /// <summary>
- /// Gets or sets the protocol.
- /// </summary>
- /// <value>The protocol.</value>
- public MediaProtocol Protocol { get; set; }
-
- /// <summary>
- /// Gets or sets the required HTTP headers.
- /// </summary>
- /// <value>The required HTTP headers.</value>
- public Dictionary<string, string> RequiredHttpHeaders { get; set; }
-
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
index 5fb982e85..7e7e759a5 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupMember.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Session;
namespace MediaBrowser.Controller.SyncPlay
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
index e3de22db3..91a13fb28 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs
index 12ce6c8f8..6b5a7cdf9 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs
index fba8ba9e2..b9786ddb0 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs
index 9797b247c..cb1cadf0b 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
index 507573653..a0c38b309 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs b/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs
index 201f29952..9045063ee 100644
--- a/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupState.cs b/MediaBrowser.Controller/SyncPlay/IGroupState.cs
index 95ee09985..0666a62a8 100644
--- a/MediaBrowser.Controller/SyncPlay/IGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/IGroupState.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
index aa263638a..de26c7d9e 100644
--- a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
+++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Threading;
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
index 1c954828c..a6999a12c 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Threading;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs
index 4090f65b9..ef496c103 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs
index 11cc99fcd..d188114c3 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs
index 64ef791ed..464c81dfd 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs
index 9cd8da566..be314e807 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs
index e0ae0deb7..679076239 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs
index 2869b35f7..7ee18a366 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs
index 8ef3b2030..beab655c5 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs
index 16f9b4087..05ff262c1 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Threading;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs
index 166ee0800..3e34b6ce4 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs
index d4af63b6d..0f91476de 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Threading;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs
index 74f01cbea..b1f0bd360 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
index 47c06c222..689145293 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Threading;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs
index ecaa689ae..196113374 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs
index c3451703e..44df127a6 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Threading;
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs
index 51011672e..d250eab56 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs
index d7b2504b4..5034e992e 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
index fdec29417..b8ae9f3ff 100644
--- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/MediaBrowser.Controller/TV/ITVSeriesManager.cs b/MediaBrowser.Controller/TV/ITVSeriesManager.cs
index 291dea04e..e066c03fd 100644
--- a/MediaBrowser.Controller/TV/ITVSeriesManager.cs
+++ b/MediaBrowser.Controller/TV/ITVSeriesManager.cs
@@ -6,16 +6,26 @@ using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.TV
{
+ /// <summary>
+ /// The TV Series manager.
+ /// </summary>
public interface ITVSeriesManager
{
/// <summary>
/// Gets the next up.
/// </summary>
+ /// <param name="query">The next up query.</param>
+ /// <param name="options">The dto options.</param>
+ /// <returns>The next up items.</returns>
QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options);
/// <summary>
/// Gets the next up.
/// </summary>
+ /// <param name="request">The next up request.</param>
+ /// <param name="parentsFolders">The list of parent folders.</param>
+ /// <param name="options">The dto options.</param>
+ /// <returns>The next up items.</returns>
QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options);
}
}
diff --git a/MediaBrowser.LocalMetadata/BaseXmlProvider.cs b/MediaBrowser.LocalMetadata/BaseXmlProvider.cs
index b036a2cc8..9f8e921b4 100644
--- a/MediaBrowser.LocalMetadata/BaseXmlProvider.cs
+++ b/MediaBrowser.LocalMetadata/BaseXmlProvider.cs
@@ -91,7 +91,7 @@ namespace MediaBrowser.LocalMetadata
/// <param name="info">Item inf.</param>
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
/// <returns>The file system metadata.</returns>
- protected abstract FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService);
+ protected abstract FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService);
/// <inheritdoc />
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
diff --git a/MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs
index 556bb6a0e..b6189bcdd 100644
--- a/MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs
@@ -35,7 +35,7 @@ namespace MediaBrowser.LocalMetadata.Images
}
/// <inheritdoc />
- public List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
+ public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
{
var collectionFolder = (CollectionFolder)item;
diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
index 393ad2efb..fdba64c4a 100644
--- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
@@ -15,17 +16,6 @@ namespace MediaBrowser.LocalMetadata.Images
/// </summary>
public class EpisodeLocalImageProvider : ILocalImageProvider, IHasOrder
{
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EpisodeLocalImageProvider"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- public EpisodeLocalImageProvider(IFileSystem fileSystem)
- {
- _fileSystem = fileSystem;
- }
-
/// <inheritdoc />
public string Name => "Local Images";
@@ -39,20 +29,24 @@ namespace MediaBrowser.LocalMetadata.Images
}
/// <inheritdoc />
- public List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
+ public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
{
var parentPath = Path.GetDirectoryName(item.Path);
+ if (parentPath == null)
+ {
+ return Enumerable.Empty<LocalImageInfo>();
+ }
var parentPathFiles = directoryService.GetFiles(parentPath);
- var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path);
+ var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
}
- private List<LocalImageInfo> GetFilesFromParentFolder(string filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
+ private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
{
- var thumbName = filenameWithoutExtension + "-thumb";
+ var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
var list = new List<LocalImageInfo>(1);
@@ -63,15 +57,15 @@ namespace MediaBrowser.LocalMetadata.Images
continue;
}
- if (BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
+ if (BaseItem.SupportedImageExtensions.Contains(i.Extension.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
- var currentNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(i);
+ var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i.FullName.AsSpan());
- if (string.Equals(filenameWithoutExtension, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+ if (filenameWithoutExtension.Equals(currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary });
}
- else if (string.Equals(thumbName, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+ else if (currentNameWithoutExtension.Equals(thumbName, StringComparison.OrdinalIgnoreCase))
{
list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary });
}
diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs
index 509b5d700..10d691b3e 100644
--- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -69,13 +70,13 @@ namespace MediaBrowser.LocalMetadata.Images
}
/// <inheritdoc />
- public List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
+ public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
{
var path = item.GetInternalMetadataPath();
if (!Directory.Exists(path))
{
- return new List<LocalImageInfo>();
+ return Enumerable.Empty<LocalImageInfo>();
}
try
@@ -85,7 +86,7 @@ namespace MediaBrowser.LocalMetadata.Images
catch (IOException ex)
{
_logger.LogError(ex, "Error while getting images for {Library}", item.Name);
- return new List<LocalImageInfo>();
+ return Enumerable.Empty<LocalImageInfo>();
}
}
}
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index 84c3ed8b0..31475e22f 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -108,7 +108,7 @@ namespace MediaBrowser.LocalMetadata.Images
{
if (!item.IsFileProtocol)
{
- return new List<FileSystemMetadata>();
+ return Enumerable.Empty<FileSystemMetadata>();
}
var path = item.ContainingFolderPath;
@@ -116,7 +116,7 @@ namespace MediaBrowser.LocalMetadata.Images
// Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs...
if (!Directory.Exists(path))
{
- return Array.Empty<FileSystemMetadata>();
+ return Enumerable.Empty<FileSystemMetadata>();
}
if (includeDirectories)
@@ -133,7 +133,7 @@ namespace MediaBrowser.LocalMetadata.Images
}
/// <inheritdoc />
- public List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
+ public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
{
var files = GetFiles(item, true, directoryService).ToList();
@@ -151,7 +151,7 @@ namespace MediaBrowser.LocalMetadata.Images
/// <param name="path">The images path.</param>
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
/// <returns>The local image info.</returns>
- public List<LocalImageInfo> GetImages(BaseItem item, string path, IDirectoryService directoryService)
+ public IEnumerable<LocalImageInfo> GetImages(BaseItem item, string path, IDirectoryService directoryService)
{
return GetImages(item, new[] { path }, directoryService);
}
@@ -163,7 +163,7 @@ namespace MediaBrowser.LocalMetadata.Images
/// <param name="paths">The image paths.</param>
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
/// <returns>The local image info.</returns>
- public List<LocalImageInfo> GetImages(BaseItem item, IEnumerable<string> paths, IDirectoryService directoryService)
+ public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IEnumerable<string> paths, IDirectoryService directoryService)
{
IEnumerable<FileSystemMetadata> files = paths.SelectMany(i => _fileSystem.GetFiles(i, BaseItem.SupportedImageExtensions, true, false));
@@ -181,9 +181,7 @@ namespace MediaBrowser.LocalMetadata.Images
{
if (supportParentSeriesFiles)
{
- var season = item as Season;
-
- if (season != null)
+ if (item is Season season)
{
PopulateSeasonImagesFromSeriesFolder(season, images, directoryService);
}
@@ -468,7 +466,7 @@ namespace MediaBrowser.LocalMetadata.Images
return added;
}
- private bool AddImage(IEnumerable<FileSystemMetadata> files, List<LocalImageInfo> images, string name, ImageType type)
+ private bool AddImage(List<FileSystemMetadata> files, List<LocalImageInfo> images, string name, ImageType type)
{
var image = GetImage(files, name);
@@ -486,9 +484,20 @@ namespace MediaBrowser.LocalMetadata.Images
return false;
}
- private FileSystemMetadata? GetImage(IEnumerable<FileSystemMetadata> files, string name)
+ private static FileSystemMetadata? GetImage(IReadOnlyList<FileSystemMetadata> files, string name)
{
- return files.FirstOrDefault(i => !i.IsDirectory && string.Equals(name, _fileSystem.GetFileNameWithoutExtension(i), StringComparison.OrdinalIgnoreCase) && i.Length > 0);
+ for (var i = 0; i < files.Count; i++)
+ {
+ var file = files[i];
+ if (!file.IsDirectory
+ && file.Length > 0
+ && Path.GetFileNameWithoutExtension(file.FullName.AsSpan()).Equals(name, StringComparison.OrdinalIgnoreCase))
+ {
+ return file;
+ }
+ }
+
+ return null;
}
}
}
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 3ce9ff4cc..eb2077a5f 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -16,6 +16,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -24,14 +26,9 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index b0afb834b..32e5ac761 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -468,8 +468,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- var hasDisplayOrder = item as IHasDisplayOrder;
- if (hasDisplayOrder != null)
+ if (item is IHasDisplayOrder hasDisplayOrder)
{
if (!string.IsNullOrWhiteSpace(val))
{
@@ -1275,8 +1274,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
// Only split by comma if there is no pipe in the string
// We have to be careful to not split names like Matthew, Jr.
- var separator = value.IndexOf('|', StringComparison.Ordinal) == -1
- && value.IndexOf(';', StringComparison.Ordinal) == -1 ? new[] { ',' } : new[] { '|', ';' };
+ var separator = !value.Contains('|', StringComparison.Ordinal)
+ && !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' };
value = value.Trim().Trim(separator);
diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
index ff846830b..7df800971 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
@@ -23,7 +23,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
/// <inheritdoc />
- protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<BoxSet> item)
+ protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<BoxSet> itemResult)
{
switch (reader.Name)
{
@@ -33,7 +33,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
using (var subReader = reader.ReadSubtree())
{
- FetchFromCollectionItemsNode(subReader, item);
+ FetchFromCollectionItemsNode(subReader, itemResult);
}
}
else
@@ -44,7 +44,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
default:
- base.FetchDataFromXmlNode(reader, item);
+ base.FetchDataFromXmlNode(reader, itemResult);
break;
}
}
diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
index 78c0fa8ad..b84307cb2 100644
--- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
@@ -23,9 +23,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
/// <inheritdoc />
- protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Playlist> result)
+ protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Playlist> itemResult)
{
- var item = result.Item;
+ var item = itemResult.Item;
switch (reader.Name)
{
@@ -53,7 +53,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
default:
- base.FetchDataFromXmlNode(reader, result);
+ base.FetchDataFromXmlNode(reader, itemResult);
break;
}
}
diff --git a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs
index cc705a9df..ea123bbf2 100644
--- a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs
+++ b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs
@@ -36,7 +36,7 @@ namespace MediaBrowser.LocalMetadata.Providers
}
/// <inheritdoc />
- protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
{
return directoryService.GetFile(Path.Combine(info.Path, "collection.xml"));
}
diff --git a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs
index 36f3048ad..899035652 100644
--- a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs
+++ b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs
@@ -39,7 +39,7 @@ namespace MediaBrowser.LocalMetadata.Providers
}
/// <inheritdoc />
- protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
{
return directoryService.GetFile(PlaylistXmlSaver.GetSavePath(info.Path));
}
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index 396206658..98ed3dcf7 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -37,7 +36,7 @@ namespace MediaBrowser.LocalMetadata.Savers
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{BaseXmlSaver}"/> interface.</param>
- public BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<BaseXmlSaver> logger)
+ protected BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<BaseXmlSaver> logger)
{
FileSystem = fileSystem;
ConfigurationManager = configurationManager;
@@ -133,7 +132,8 @@ namespace MediaBrowser.LocalMetadata.Savers
// On Windows, savint the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
- using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
stream.CopyTo(filestream);
}
@@ -214,7 +214,7 @@ namespace MediaBrowser.LocalMetadata.Savers
if (item.LockedFields.Length > 0)
{
- writer.WriteElementString("LockedFields", string.Join("|", item.LockedFields));
+ writer.WriteElementString("LockedFields", string.Join('|', item.LockedFields));
}
if (item.CriticRating.HasValue)
@@ -296,8 +296,7 @@ namespace MediaBrowser.LocalMetadata.Savers
writer.WriteEndElement();
}
- var hasDisplayOrder = item as IHasDisplayOrder;
- if (hasDisplayOrder != null && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
+ if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
{
writer.WriteElementString("DisplayOrder", hasDisplayOrder.DisplayOrder);
}
@@ -312,8 +311,7 @@ namespace MediaBrowser.LocalMetadata.Savers
writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(_usCulture));
}
- var hasAspectRatio = item as IHasAspectRatio;
- if (hasAspectRatio != null)
+ if (item is IHasAspectRatio hasAspectRatio)
{
if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
{
@@ -420,20 +418,17 @@ namespace MediaBrowser.LocalMetadata.Savers
writer.WriteEndElement();
}
- var boxset = item as BoxSet;
- if (boxset != null)
+ if (item is BoxSet boxset)
{
AddLinkedChildren(boxset, writer, "CollectionItems", "CollectionItem");
}
- var playlist = item as Playlist;
- if (playlist != null && !Playlist.IsPlaylistFile(playlist.Path))
+ if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path))
{
AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem");
}
- var hasShares = item as IHasShares;
- if (hasShares != null)
+ if (item is IHasShares hasShares)
{
AddShares(hasShares, writer);
}
@@ -541,10 +536,5 @@ namespace MediaBrowser.LocalMetadata.Savers
writer.WriteEndElement();
}
-
- private bool IsPersonType(PersonInfo person, string type)
- {
- return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
- }
}
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index e8aeabf9d..a0ec3bd90 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index 4a54b677d..e86e518be 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -9,9 +9,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo
{
public class BdInfoDirectoryInfo : IDirectoryInfo
{
- private readonly IFileSystem _fileSystem = null;
+ private readonly IFileSystem _fileSystem;
- private readonly FileSystemMetadata _impl = null;
+ private readonly FileSystemMetadata _impl;
public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
{
@@ -29,7 +29,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
public string FullName => _impl.FullName;
- public IDirectoryInfo Parent
+ public IDirectoryInfo? Parent
{
get
{
@@ -71,7 +71,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
_impl.FullName,
new[] { searchPattern },
false,
- searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(),
+ (searchOption & System.IO.SearchOption.AllDirectories) == System.IO.SearchOption.AllDirectories).ToArray(),
x => new BdInfoFileInfo(x));
}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
index 9108d9649..6ebaa4fff 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
@@ -61,33 +61,25 @@ namespace MediaBrowser.MediaEncoding.BdInfo
foreach (var stream in playlist.SortedStreams)
{
- var videoStream = stream as TSVideoStream;
-
- if (videoStream != null)
+ if (stream is TSVideoStream videoStream)
{
AddVideoStream(mediaStreams, videoStream);
continue;
}
- var audioStream = stream as TSAudioStream;
-
- if (audioStream != null)
+ if (stream is TSAudioStream audioStream)
{
AddAudioStream(mediaStreams, audioStream);
continue;
}
- var textStream = stream as TSTextStream;
-
- if (textStream != null)
+ if (stream is TSTextStream textStream)
{
AddSubtitleStream(mediaStreams, textStream);
continue;
}
- var graphicsStream = stream as TSGraphicsStream;
-
- if (graphicsStream != null)
+ if (stream is TSGraphicsStream graphicsStream)
{
AddSubtitleStream(mediaStreams, graphicsStream);
}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index 0a8af8e9c..41143c259 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -7,7 +7,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
{
public class BdInfoFileInfo : BDInfo.IO.IFileInfo
{
- private FileSystemMetadata _impl = null;
+ private FileSystemMetadata _impl;
public BdInfoFileInfo(FileSystemMetadata impl)
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 9e2417603..f782e65bd 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -121,11 +121,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
// When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions
public static Version MinVersion { get; } = new Version(4, 0);
- public static Version MaxVersion { get; } = null;
+ public static Version? MaxVersion { get; } = null;
public bool ValidateVersion()
{
- string output = null;
+ string output;
try
{
output = GetProcessOutput(_encoderPath, "-version");
@@ -133,6 +133,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
catch (Exception ex)
{
_logger.LogError(ex, "Error validating encoder");
+ return false;
}
if (string.IsNullOrWhiteSpace(output))
@@ -207,7 +208,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="output">The output from "ffmpeg -version".</param>
/// <returns>The FFmpeg version.</returns>
- internal Version GetFFmpegVersion(string output)
+ internal Version? GetFFmpegVersion(string output)
{
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
@@ -275,7 +276,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private IEnumerable<string> GetHwaccelTypes()
{
- string output = null;
+ string? output = null;
try
{
output = GetProcessOutput(_encoderPath, "-hwaccels");
@@ -303,7 +304,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
- string output = null;
+ string output;
try
{
output = GetProcessOutput(_encoderPath, "-h filter=" + filter);
@@ -311,6 +312,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
catch (Exception ex)
{
_logger.LogError(ex, "Error detecting the given filter");
+ return false;
}
if (output.Contains("Filter " + filter, StringComparison.Ordinal))
@@ -331,7 +333,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private IEnumerable<string> GetCodecs(Codec codec)
{
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
- string output = null;
+ string output;
try
{
output = GetProcessOutput(_encoderPath, "-" + codecstr);
@@ -339,6 +341,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
catch (Exception ex)
{
_logger.LogError(ex, "Error detecting available {Codec}", codecstr);
+ return Enumerable.Empty<string>();
}
if (string.IsNullOrWhiteSpace(output))
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index c0b6cf28b..3af618af8 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
@@ -16,7 +17,6 @@ using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Probing;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -53,7 +53,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
- private readonly Lazy<EncodingHelper> _encodingHelperFactory;
private readonly string _startupOptionFFmpegPath;
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2);
@@ -77,16 +76,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
ILocalizationManager localization,
- Lazy<EncodingHelper> encodingHelperFactory,
IConfiguration config)
{
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
_localization = localization;
- _encodingHelperFactory = encodingHelperFactory;
_startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty;
- _jsonSerializerOptions = JsonDefaults.GetOptions();
+ _jsonSerializerOptions = JsonDefaults.Options;
}
/// <inheritdoc />
@@ -103,7 +100,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
public void SetFFmpegPath()
{
// 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
- if (!ValidatePath(_configurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
+ if (!ValidatePath(_configurationManager.GetEncodingOptions().EncoderAppPath, FFmpegLocation.Custom))
{
// 2) Check if the --ffmpeg CLI switch has been given
if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument))
@@ -118,7 +115,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
// Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
- var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding");
+ var config = _configurationManager.GetEncodingOptions();
config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
_configurationManager.SaveConfiguration("encoding", config);
@@ -177,7 +174,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Write the new ffmpeg path to the xml as <EncoderAppPath>
// This ensures its not lost on next startup
- var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding");
+ var config = _configurationManager.GetEncodingOptions();
config.EncoderAppPath = newPath;
_configurationManager.SaveConfiguration("encoding", config);
@@ -209,6 +206,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_ffmpegPath = path;
EncoderLocation = location;
+ return true;
}
else
{
@@ -369,7 +367,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
{
var prefix = "file";
- if (mediaSource.VideoType == VideoType.BluRay || mediaSource.VideoType == VideoType.Iso)
+ if (mediaSource.VideoType == VideoType.BluRay
+ || mediaSource.IsoType == IsoType.BluRay)
{
prefix = "bluray";
}
@@ -447,7 +446,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (result == null || (result.Streams == null && result.Format == null))
{
- throw new Exception("ffprobe failed - streams and format are both null.");
+ throw new FfmpegException("ffprobe failed - streams and format are both null.");
}
if (result.Streams != null)
@@ -570,32 +569,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar.
// This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar
- var vf = string.Empty;
-
- if (threedFormat.HasValue)
+ var vf = threedFormat switch
{
- switch (threedFormat.Value)
- {
- case Video3DFormat.HalfSideBySide:
- vf = "-vf crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
- // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
- break;
- case Video3DFormat.FullSideBySide:
- vf = "-vf crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
- // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
- break;
- case Video3DFormat.HalfTopAndBottom:
- vf = "-vf crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
- // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made
- break;
- case Video3DFormat.FullTopAndBottom:
- vf = "-vf crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
- // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made
- break;
- default:
- break;
- }
- }
+ // 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 => "-vf crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
+ // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
+ Video3DFormat.FullSideBySide => "-vf crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
+ // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made
+ Video3DFormat.HalfTopAndBottom => "-vf crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
+ // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made
+ Video3DFormat.FullTopAndBottom => "-vf crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
+ _ => string.Empty
+ };
var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
@@ -603,7 +588,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (enableHdrExtraction)
{
string tonemapFilters = "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p";
- if (string.IsNullOrEmpty(vf))
+ if (vf.Length == 0)
{
vf = "-vf " + tonemapFilters;
}
@@ -632,35 +617,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads);
- var probeSizeArgument = string.Empty;
- var analyzeDurationArgument = string.Empty;
-
- if (!string.IsNullOrWhiteSpace(probeSizeArgument))
- {
- args = probeSizeArgument + " " + args;
- }
-
- if (!string.IsNullOrWhiteSpace(analyzeDurationArgument))
- {
- args = analyzeDurationArgument + " " + args;
- }
-
if (offset.HasValue)
{
args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args;
}
- if (videoStream != null)
- {
- /* fix
- var decoder = encodinghelper.GetHardwareAcceleratedVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions());
- if (!string.IsNullOrWhiteSpace(decoder))
- {
- args = decoder + " " + args;
- }
- */
- }
-
if (!string.IsNullOrWhiteSpace(container))
{
var inputFormat = EncodingHelper.GetInputFormat(container);
@@ -722,7 +683,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogError(msg);
- throw new Exception(msg);
+ throw new FfmpegException(msg);
}
return tempExtractPath;
@@ -769,30 +730,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads);
- var probeSizeArgument = string.Empty;
- var analyzeDurationArgument = string.Empty;
-
- if (!string.IsNullOrWhiteSpace(probeSizeArgument))
- {
- args = probeSizeArgument + " " + args;
- }
-
- if (!string.IsNullOrWhiteSpace(analyzeDurationArgument))
- {
- args = analyzeDurationArgument + " " + args;
- }
-
- if (videoStream != null)
- {
- /* fix
- var decoder = encodinghelper.GetHardwareAcceleratedVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions());
- if (!string.IsNullOrWhiteSpace(decoder))
- {
- args = decoder + " " + args;
- }
- */
- }
-
if (!string.IsNullOrWhiteSpace(container))
{
var inputFormat = EncodingHelper.GetInputFormat(container);
@@ -871,7 +808,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogError(msg);
- throw new Exception(msg);
+ throw new FfmpegException(msg);
}
}
}
diff --git a/MediaBrowser.MediaEncoding/FfmpegException.cs b/MediaBrowser.MediaEncoding/FfmpegException.cs
new file mode 100644
index 000000000..1697fd33a
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/FfmpegException.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace MediaBrowser.MediaEncoding
+{
+ /// <summary>
+ /// Represents errors that occur during interaction with FFmpeg.
+ /// </summary>
+ public class FfmpegException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FfmpegException"/> class.
+ /// </summary>
+ public FfmpegException()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ public FfmpegException(string message) : base(message)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message and a
+ /// reference to the inner exception that is the cause of this exception.
+ /// </summary>
+ /// <param name="message">The error message that explains the reason for the exception.</param>
+ /// <param name="innerException">
+ /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
+ /// no inner exception is specified.
+ /// </param>
+ public FfmpegException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index f8af499e4..7733e715f 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -10,6 +10,9 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -24,18 +27,14 @@
<ItemGroup>
<PackageReference Include="BDInfo" Version="0.7.6.1" />
+ <PackageReference Include="libse" Version="3.6.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
<PackageReference Include="UTF.Unknown" Version="2.3.0" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index b2d4db894..1fa90bb21 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -1,5 +1,8 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
+using System.Globalization;
namespace MediaBrowser.MediaEncoding.Probing
{
@@ -85,12 +88,14 @@ namespace MediaBrowser.MediaEncoding.Probing
{
var val = GetDictionaryValue(tags, key);
- if (!string.IsNullOrEmpty(val))
+ if (string.IsNullOrEmpty(val))
{
- if (DateTime.TryParse(val, out var i))
- {
- return i.ToUniversalTime();
- }
+ return null;
+ }
+
+ if (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var i))
+ {
+ return i.ToUniversalTime();
}
return null;
diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
index 0e319c1a8..d4d153b08 100644
--- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
+++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
index de062d06b..a1cef7a9f 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
index 8af122ef9..d50da37b8 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index 8a7c032c5..c9c8c34c2 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -1,6 +1,7 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Common.Json.Converters;
namespace MediaBrowser.MediaEncoding.Probing
{
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index bd026bce1..bbff5daca 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
@@ -29,7 +30,7 @@ namespace MediaBrowser.MediaEncoding.Probing
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
- private List<string> _splitWhiteList = null;
+ private string[] _splitWhiteList;
public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization)
{
@@ -37,6 +38,8 @@ namespace MediaBrowser.MediaEncoding.Probing
_localization = localization;
}
+ private IReadOnlyList<string> SplitWhitelist => _splitWhiteList ??= new string[] { "AC/DC" };
+
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
{
var info = new MediaInfo
@@ -57,7 +60,7 @@ namespace MediaBrowser.MediaEncoding.Probing
.Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec))
.ToList();
- info.MediaAttachments = internalStreams.Select(s => GetMediaAttachment(s))
+ info.MediaAttachments = internalStreams.Select(GetMediaAttachment)
.Where(i => i != null)
.ToList();
@@ -121,18 +124,67 @@ namespace MediaBrowser.MediaEncoding.Probing
{
info.Name = title;
}
+ else
+ {
+ title = FFProbeHelpers.GetDictionaryValue(tags, "title-eng");
+ if (!string.IsNullOrWhiteSpace(title))
+ {
+ info.Name = title;
+ }
+ }
+
+ var titleSort = FFProbeHelpers.GetDictionaryValue(tags, "titlesort");
+ if (!string.IsNullOrWhiteSpace(titleSort))
+ {
+ info.ForcedSortName = titleSort;
+ }
info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort");
info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number");
info.ShowName = FFProbeHelpers.GetDictionaryValue(tags, "show_name");
info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
- // Several different forms of retaildate
- info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
+ // Several different forms of retail/premiere date
+ info.PremiereDate =
+ FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
+ FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
+ // Set common metadata for music (audio) and music videos (video)
+ info.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
+
+ var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
+
+ if (!string.IsNullOrWhiteSpace(artists))
+ {
+ info.Artists = SplitArtists(artists, new[] { '/', ';' }, false)
+ .DistinctNames()
+ .ToArray();
+ }
+ else
+ {
+ var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
+ if (string.IsNullOrWhiteSpace(artist))
+ {
+ info.Artists = Array.Empty<string>();
+ }
+ else
+ {
+ info.Artists = SplitArtists(artist, _nameDelimiters, true)
+ .DistinctNames()
+ .ToArray();
+ }
+ }
+
+ // If we don't have a ProductionYear try and get it from PremiereDate
+ if (!info.ProductionYear.HasValue && info.PremiereDate.HasValue)
+ {
+ info.ProductionYear = info.PremiereDate.Value.Year;
+ }
+
+ // Set mediaType-specific metadata
if (isAudio)
{
SetAudioRuntimeTicks(data, info);
@@ -640,7 +692,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
// Filter out junk
- if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && streamInfo.CodecTagString.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1)
+ if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase))
{
stream.CodecTag = streamInfo.CodecTagString;
}
@@ -681,9 +733,9 @@ namespace MediaBrowser.MediaEncoding.Probing
{
stream.Type = MediaStreamType.Subtitle;
stream.Codec = NormalizeSubtitleCodec(stream.Codec);
- stream.localizedUndefined = _localization.GetLocalizedString("Undefined");
- stream.localizedDefault = _localization.GetLocalizedString("Default");
- stream.localizedForced = _localization.GetLocalizedString("Forced");
+ stream.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
+ stream.LocalizedDefault = _localization.GetLocalizedString("Default");
+ stream.LocalizedForced = _localization.GetLocalizedString("Forced");
}
else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
{
@@ -1075,13 +1127,13 @@ namespace MediaBrowser.MediaEncoding.Probing
private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags)
{
- var peoples = new List<BaseItemPerson>();
+ var people = new List<BaseItemPerson>();
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
if (!string.IsNullOrWhiteSpace(composer))
{
foreach (var person in Split(composer, false))
{
- peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
}
}
@@ -1090,7 +1142,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(conductor, false))
{
- peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
}
}
@@ -1099,46 +1151,21 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(lyricist, false))
{
- peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
}
}
// Check for writer some music is tagged that way as alternative to composer/lyricist
var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer");
-
if (!string.IsNullOrWhiteSpace(writer))
{
foreach (var person in Split(writer, false))
{
- peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
}
}
- audio.People = peoples.ToArray();
- audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
-
- var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
-
- if (!string.IsNullOrWhiteSpace(artists))
- {
- audio.Artists = SplitArtists(artists, new[] { '/', ';' }, false)
- .DistinctNames()
- .ToArray();
- }
- else
- {
- var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
- if (string.IsNullOrWhiteSpace(artist))
- {
- audio.Artists = Array.Empty<string>();
- }
- else
- {
- audio.Artists = SplitArtists(artist, _nameDelimiters, true)
- .DistinctNames()
- .ToArray();
- }
- }
+ audio.People = people.ToArray();
var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
if (string.IsNullOrWhiteSpace(albumArtist))
@@ -1173,12 +1200,6 @@ namespace MediaBrowser.MediaEncoding.Probing
// Disc number
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
- // If we don't have a ProductionYear try and get it from PremiereDate
- if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
- {
- audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
- }
-
// There's several values in tags may or may not be present
FetchStudios(audio, tags, "organization");
FetchStudios(audio, tags, "ensemble");
@@ -1186,43 +1207,28 @@ namespace MediaBrowser.MediaEncoding.Probing
FetchStudios(audio, tags, "label");
// These support mulitple values, but for now we only store the first.
- var mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id"));
- if (mb == null)
- {
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMARTISTID"));
- }
+ var mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id"))
+ ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMARTISTID"));
audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb);
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id"));
- if (mb == null)
- {
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ARTISTID"));
- }
+ mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id"))
+ ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ARTISTID"));
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb);
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id"));
- if (mb == null)
- {
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMID"));
- }
+ mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id"))
+ ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMID"));
audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb);
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id"));
- if (mb == null)
- {
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASEGROUPID"));
- }
+ mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id"))
+ ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASEGROUPID"));
audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb);
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id"));
- if (mb == null)
- {
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASETRACKID"));
- }
+ mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id"))
+ ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASETRACKID"));
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb);
}
@@ -1268,7 +1274,7 @@ namespace MediaBrowser.MediaEncoding.Probing
var artistsFound = new List<string>();
- foreach (var whitelistArtist in GetSplitWhitelist())
+ foreach (var whitelistArtist in SplitWhitelist)
{
var originalVal = val;
val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase);
@@ -1287,19 +1293,6 @@ namespace MediaBrowser.MediaEncoding.Probing
return artistsFound;
}
- private IEnumerable<string> GetSplitWhitelist()
- {
- if (_splitWhiteList == null)
- {
- _splitWhiteList = new List<string>
- {
- "AC/DC"
- };
- }
-
- return _splitWhiteList;
- }
-
/// <summary>
/// Gets the studios from the tags collection.
/// </summary>
@@ -1496,19 +1489,31 @@ namespace MediaBrowser.MediaEncoding.Probing
video.IndexNumber = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[0], CultureInfo.InvariantCulture);
int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[1], CultureInfo.InvariantCulture);
- description = string.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
+ description = string.Join(' ', numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
}
else
{
- throw new Exception(); // Switch to default parsing
+ // Switch to default parsing
+ if (subtitle.Contains('.', StringComparison.Ordinal))
+ {
+ // skip the comment, keep the subtitle
+ description = string.Join('.', subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
+ }
+ else
+ {
+ description = subtitle.Trim(); // Clean up whitespaces and save it
+ }
}
}
- catch // Default parsing
+ catch (Exception ex)
{
+ _logger.LogError(ex, "Error while parsing subtitle field");
+
+ // Default parsing
if (subtitle.Contains('.', StringComparison.Ordinal))
{
// skip the comment, keep the subtitle
- description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
+ description = string.Join('.', subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
}
else
{
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
index bb48bed27..08ee5c72e 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
@@ -1,130 +1,19 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public class AssParser : ISubtitleParser
+ /// <summary>
+ /// Advanced SubStation Alpha subtitle parser.
+ /// </summary>
+ public class AssParser : SubtitleEditParser<AdvancedSubStationAlpha>
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- /// <inheritdoc />
- public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
- {
- var trackInfo = new SubtitleTrackInfo();
- var trackEvents = new List<SubtitleTrackEvent>();
- var eventIndex = 1;
- using (var reader = new StreamReader(stream))
- {
- string line;
- while (!string.Equals(reader.ReadLine(), "[Events]", StringComparison.Ordinal))
- {
- }
-
- var headers = ParseFieldHeaders(reader.ReadLine());
-
- while ((line = reader.ReadLine()) != null)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (string.IsNullOrWhiteSpace(line))
- {
- continue;
- }
-
- if (line[0] == '[')
- {
- break;
- }
-
- var subEvent = new SubtitleTrackEvent { Id = eventIndex.ToString(_usCulture) };
- eventIndex++;
- const string Dialogue = "Dialogue: ";
- var sections = line.Substring(Dialogue.Length).Split(',');
-
- subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]);
- subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]);
-
- subEvent.Text = string.Join(',', sections[headers["Text"]..]);
- RemoteNativeFormatting(subEvent);
-
- subEvent.Text = subEvent.Text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
-
- subEvent.Text = Regex.Replace(subEvent.Text, @"\{(\\[\w]+\(?([\w0-9]+,?)+\)?)+\}", string.Empty, RegexOptions.IgnoreCase);
-
- trackEvents.Add(subEvent);
- }
- }
-
- trackInfo.TrackEvents = trackEvents;
- return trackInfo;
- }
-
- private long GetTicks(ReadOnlySpan<char> time)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AssParser"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ public AssParser(ILogger logger) : base(logger)
{
- return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out var span)
- ? span.Ticks : 0;
- }
-
- internal static Dictionary<string, int> ParseFieldHeaders(string line)
- {
- const string Format = "Format: ";
- var fields = line.Substring(Format.Length).Split(',').Select(x => x.Trim()).ToList();
-
- return new Dictionary<string, int>
- {
- { "Start", fields.IndexOf("Start") },
- { "End", fields.IndexOf("End") },
- { "Text", fields.IndexOf("Text") }
- };
- }
-
- private void RemoteNativeFormatting(SubtitleTrackEvent p)
- {
- int indexOfBegin = p.Text.IndexOf('{', StringComparison.Ordinal);
- string pre = string.Empty;
- while (indexOfBegin >= 0 && p.Text.IndexOf('}', StringComparison.Ordinal) > indexOfBegin)
- {
- string s = p.Text.Substring(indexOfBegin);
- if (s.StartsWith("{\\an1}", StringComparison.Ordinal) ||
- s.StartsWith("{\\an2}", StringComparison.Ordinal) ||
- s.StartsWith("{\\an3}", StringComparison.Ordinal) ||
- s.StartsWith("{\\an4}", StringComparison.Ordinal) ||
- s.StartsWith("{\\an5}", StringComparison.Ordinal) ||
- s.StartsWith("{\\an6}", StringComparison.Ordinal) ||
- s.StartsWith("{\\an7}", StringComparison.Ordinal) ||
- s.StartsWith("{\\an8}", StringComparison.Ordinal) ||
- s.StartsWith("{\\an9}", StringComparison.Ordinal))
- {
- pre = s.Substring(0, 6);
- }
- else if (s.StartsWith("{\\an1\\", StringComparison.Ordinal) ||
- s.StartsWith("{\\an2\\", StringComparison.Ordinal) ||
- s.StartsWith("{\\an3\\", StringComparison.Ordinal) ||
- s.StartsWith("{\\an4\\", StringComparison.Ordinal) ||
- s.StartsWith("{\\an5\\", StringComparison.Ordinal) ||
- s.StartsWith("{\\an6\\", StringComparison.Ordinal) ||
- s.StartsWith("{\\an7\\", StringComparison.Ordinal) ||
- s.StartsWith("{\\an8\\", StringComparison.Ordinal) ||
- s.StartsWith("{\\an9\\", StringComparison.Ordinal))
- {
- pre = s.Substring(0, 5) + "}";
- }
-
- int indexOfEnd = p.Text.IndexOf('}', StringComparison.Ordinal);
- p.Text = p.Text.Remove(indexOfBegin, (indexOfEnd - indexOfBegin) + 1);
-
- indexOfBegin = p.Text.IndexOf('{', StringComparison.Ordinal);
- }
-
- p.Text = pre + p.Text;
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
deleted file mode 100644
index dca5c1e8a..000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-#nullable enable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- public static class ParserValues
- {
- public const string NewLine = "\r\n";
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
index ccef7eeea..78d54ca51 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
@@ -1,102 +1,19 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public class SrtParser : ISubtitleParser
+ /// <summary>
+ /// SubRip subtitle parser.
+ /// </summary>
+ public class SrtParser : SubtitleEditParser<SubRip>
{
- private readonly ILogger _logger;
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- public SrtParser(ILogger logger)
- {
- _logger = logger;
- }
-
- /// <inheritdoc />
- public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
- {
- var trackInfo = new SubtitleTrackInfo();
- var trackEvents = new List<SubtitleTrackEvent>();
- using (var reader = new StreamReader(stream))
- {
- string line;
- while ((line = reader.ReadLine()) != null)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (string.IsNullOrWhiteSpace(line))
- {
- continue;
- }
-
- var subEvent = new SubtitleTrackEvent { Id = line };
- line = reader.ReadLine();
-
- if (string.IsNullOrWhiteSpace(line))
- {
- continue;
- }
-
- var time = Regex.Split(line, @"[\t ]*-->[\t ]*");
-
- if (time.Length < 2)
- {
- // This occurs when subtitle text has an empty line as part of the text.
- // Need to adjust the break statement below to resolve this.
- _logger.LogWarning("Unrecognized line in srt: {0}", line);
- continue;
- }
-
- subEvent.StartPositionTicks = GetTicks(time[0]);
- var endTime = time[1].AsSpan();
- var idx = endTime.IndexOf(' ');
- if (idx > 0)
- {
- endTime = endTime.Slice(0, idx);
- }
-
- subEvent.EndPositionTicks = GetTicks(endTime);
- var multiline = new List<string>();
- while ((line = reader.ReadLine()) != null)
- {
- if (line.Length == 0)
- {
- break;
- }
-
- multiline.Add(line);
- }
-
- subEvent.Text = string.Join(ParserValues.NewLine, multiline);
- subEvent.Text = subEvent.Text.Replace(@"\N", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
- subEvent.Text = Regex.Replace(subEvent.Text, @"\{(?:\\[0-9]?[\w.-]+(?:\([^\)]*\)|&H?[0-9A-Fa-f]+&|))+\}", string.Empty, RegexOptions.IgnoreCase);
- subEvent.Text = Regex.Replace(subEvent.Text, "<", "&lt;", RegexOptions.IgnoreCase);
- subEvent.Text = Regex.Replace(subEvent.Text, ">", "&gt;", RegexOptions.IgnoreCase);
- subEvent.Text = Regex.Replace(subEvent.Text, "&lt;(\\/?(font|b|u|i|s))((\\s+(\\w|\\w[\\w\\-]*\\w)(\\s*=\\s*(?:\\\".*?\\\"|'.*?'|[^'\\\">\\s]+))?)+\\s*|\\s*)(\\/?)&gt;", "<$1$3$7>", RegexOptions.IgnoreCase);
- trackEvents.Add(subEvent);
- }
- }
-
- trackInfo.TrackEvents = trackEvents;
- return trackInfo;
- }
-
- private long GetTicks(ReadOnlySpan<char> time)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SrtParser"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ public SrtParser(ILogger logger) : base(logger)
{
- return TimeSpan.TryParseExact(time, @"hh\:mm\:ss\.fff", _usCulture, out var span)
- ? span.Ticks
- : (TimeSpan.TryParseExact(time, @"hh\:mm\:ss\,fff", _usCulture, out span)
- ? span.Ticks : 0);
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
index bc84c5074..17c2ae40e 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
@@ -1,477 +1,19 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
- /// <see href="https://github.com/SubtitleEdit/subtitleedit/blob/a299dc4407a31796364cc6ad83f0d3786194ba22/src/Logic/SubtitleFormats/SubStationAlpha.cs">Credit</see>.
+ /// SubStation Alpha subtitle parser.
/// </summary>
- public class SsaParser : ISubtitleParser
+ public class SsaParser : SubtitleEditParser<SubStationAlpha>
{
- /// <inheritdoc />
- public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SsaParser"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ public SsaParser(ILogger logger) : base(logger)
{
- var trackInfo = new SubtitleTrackInfo();
- var trackEvents = new List<SubtitleTrackEvent>();
-
- using (var reader = new StreamReader(stream))
- {
- bool eventsStarted = false;
-
- string[] format = "Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text".Split(',');
- int indexLayer = 0;
- int indexStart = 1;
- int indexEnd = 2;
- int indexStyle = 3;
- int indexName = 4;
- int indexEffect = 8;
- int indexText = 9;
- int lineNumber = 0;
-
- var header = new StringBuilder();
-
- string line;
-
- while ((line = reader.ReadLine()) != null)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- lineNumber++;
- if (!eventsStarted)
- {
- header.AppendLine(line);
- }
-
- if (string.Equals(line.Trim(), "[events]", StringComparison.OrdinalIgnoreCase))
- {
- eventsStarted = true;
- }
- else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(';'))
- {
- // skip comment lines
- }
- else if (eventsStarted && line.Trim().Length > 0)
- {
- string s = line.Trim().ToLowerInvariant();
- if (s.StartsWith("format:", StringComparison.Ordinal))
- {
- if (line.Length > 10)
- {
- format = line.ToLowerInvariant().Substring(8).Split(',');
- for (int i = 0; i < format.Length; i++)
- {
- if (string.Equals(format[i].Trim(), "layer", StringComparison.OrdinalIgnoreCase))
- {
- indexLayer = i;
- }
- else if (string.Equals(format[i].Trim(), "start", StringComparison.OrdinalIgnoreCase))
- {
- indexStart = i;
- }
- else if (string.Equals(format[i].Trim(), "end", StringComparison.OrdinalIgnoreCase))
- {
- indexEnd = i;
- }
- else if (string.Equals(format[i].Trim(), "text", StringComparison.OrdinalIgnoreCase))
- {
- indexText = i;
- }
- else if (string.Equals(format[i].Trim(), "effect", StringComparison.OrdinalIgnoreCase))
- {
- indexEffect = i;
- }
- else if (string.Equals(format[i].Trim(), "style", StringComparison.OrdinalIgnoreCase))
- {
- indexStyle = i;
- }
- }
- }
- }
- else if (!string.IsNullOrEmpty(s))
- {
- string text = string.Empty;
- string start = string.Empty;
- string end = string.Empty;
- string style = string.Empty;
- string layer = string.Empty;
- string effect = string.Empty;
- string name = string.Empty;
-
- string[] splittedLine;
-
- if (s.StartsWith("dialogue:", StringComparison.Ordinal))
- {
- splittedLine = line.Substring(10).Split(',');
- }
- else
- {
- splittedLine = line.Split(',');
- }
-
- for (int i = 0; i < splittedLine.Length; i++)
- {
- if (i == indexStart)
- {
- start = splittedLine[i].Trim();
- }
- else if (i == indexEnd)
- {
- end = splittedLine[i].Trim();
- }
- else if (i == indexLayer)
- {
- layer = splittedLine[i];
- }
- else if (i == indexEffect)
- {
- effect = splittedLine[i];
- }
- else if (i == indexText)
- {
- text = splittedLine[i];
- }
- else if (i == indexStyle)
- {
- style = splittedLine[i];
- }
- else if (i == indexName)
- {
- name = splittedLine[i];
- }
- else if (i > indexText)
- {
- text += "," + splittedLine[i];
- }
- }
-
- try
- {
- trackEvents.Add(
- new SubtitleTrackEvent
- {
- StartPositionTicks = GetTimeCodeFromString(start),
- EndPositionTicks = GetTimeCodeFromString(end),
- Text = GetFormattedText(text)
- });
- }
- catch
- {
- }
- }
- }
- }
-
- // if (header.Length > 0)
- // subtitle.Header = header.ToString();
-
- // subtitle.Renumber(1);
- }
-
- trackInfo.TrackEvents = trackEvents.ToArray();
- return trackInfo;
- }
-
- private static long GetTimeCodeFromString(string time)
- {
- // h:mm:ss.cc
- string[] timeCode = time.Split(':', '.');
- return new TimeSpan(
- 0,
- int.Parse(timeCode[0], CultureInfo.InvariantCulture),
- int.Parse(timeCode[1], CultureInfo.InvariantCulture),
- int.Parse(timeCode[2], CultureInfo.InvariantCulture),
- int.Parse(timeCode[3], CultureInfo.InvariantCulture) * 10).Ticks;
- }
-
- private static string GetFormattedText(string text)
- {
- text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
-
- for (int i = 0; i < 10; i++) // just look ten times...
- {
- if (text.Contains(@"{\fn", StringComparison.Ordinal))
- {
- int start = text.IndexOf(@"{\fn", StringComparison.Ordinal);
- int end = text.IndexOf('}', start);
- if (end > 0 && !text.Substring(start).StartsWith("{\\fn}", StringComparison.Ordinal))
- {
- string fontName = text.Substring(start + 4, end - (start + 4));
- string extraTags = string.Empty;
- CheckAndAddSubTags(ref fontName, ref extraTags, out bool italic);
- text = text.Remove(start, end - start + 1);
- if (italic)
- {
- text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + "><i>");
- }
- else
- {
- text = text.Insert(start, "<font face=\"" + fontName + "\"" + extraTags + ">");
- }
-
- int indexOfEndTag = text.IndexOf("{\\fn}", start, StringComparison.Ordinal);
- if (indexOfEndTag > 0)
- {
- text = text.Remove(indexOfEndTag, "{\\fn}".Length).Insert(indexOfEndTag, "</font>");
- }
- else
- {
- text += "</font>";
- }
- }
- }
-
- if (text.Contains(@"{\fs", StringComparison.Ordinal))
- {
- int start = text.IndexOf(@"{\fs", StringComparison.Ordinal);
- int end = text.IndexOf('}', start);
- if (end > 0 && !text.Substring(start).StartsWith("{\\fs}", StringComparison.Ordinal))
- {
- string fontSize = text.Substring(start + 4, end - (start + 4));
- string extraTags = string.Empty;
- CheckAndAddSubTags(ref fontSize, ref extraTags, out bool italic);
- if (IsInteger(fontSize))
- {
- text = text.Remove(start, end - start + 1);
- if (italic)
- {
- text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + "><i>");
- }
- else
- {
- text = text.Insert(start, "<font size=\"" + fontSize + "\"" + extraTags + ">");
- }
-
- int indexOfEndTag = text.IndexOf("{\\fs}", start, StringComparison.Ordinal);
- if (indexOfEndTag > 0)
- {
- text = text.Remove(indexOfEndTag, "{\\fs}".Length).Insert(indexOfEndTag, "</font>");
- }
- else
- {
- text += "</font>";
- }
- }
- }
- }
-
- if (text.Contains(@"{\c", StringComparison.Ordinal))
- {
- int start = text.IndexOf(@"{\c", StringComparison.Ordinal);
- int end = text.IndexOf('}', start);
- if (end > 0 && !text.Substring(start).StartsWith("{\\c}", StringComparison.Ordinal))
- {
- string color = text.Substring(start + 4, end - (start + 4));
- string extraTags = string.Empty;
- CheckAndAddSubTags(ref color, ref extraTags, out bool italic);
-
- color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H');
- color = color.PadLeft(6, '0');
-
- // switch to rrggbb from bbggrr
- color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
- color = color.ToLowerInvariant();
-
- text = text.Remove(start, end - start + 1);
- if (italic)
- {
- text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>");
- }
- else
- {
- text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
- }
-
- int indexOfEndTag = text.IndexOf("{\\c}", start, StringComparison.Ordinal);
- if (indexOfEndTag > 0)
- {
- text = text.Remove(indexOfEndTag, "{\\c}".Length).Insert(indexOfEndTag, "</font>");
- }
- else
- {
- text += "</font>";
- }
- }
- }
-
- if (text.Contains(@"{\1c", StringComparison.Ordinal)) // "1" specifices primary color
- {
- int start = text.IndexOf(@"{\1c", StringComparison.Ordinal);
- int end = text.IndexOf('}', start);
- if (end > 0 && !text.Substring(start).StartsWith("{\\1c}", StringComparison.Ordinal))
- {
- string color = text.Substring(start + 5, end - (start + 5));
- string extraTags = string.Empty;
- CheckAndAddSubTags(ref color, ref extraTags, out bool italic);
-
- color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H');
- color = color.PadLeft(6, '0');
-
- // switch to rrggbb from bbggrr
- color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
- color = color.ToLowerInvariant();
-
- text = text.Remove(start, end - start + 1);
- if (italic)
- {
- text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + "><i>");
- }
- else
- {
- text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
- }
-
- int indexOfEndTag = text.IndexOf("{\\1c}", start, StringComparison.Ordinal);
- if (indexOfEndTag > 0)
- {
- text = text.Remove(indexOfEndTag, "{\\1c}".Length).Insert(indexOfEndTag, "</font>");
- }
- else
- {
- text += "</font>";
- }
- }
- }
- }
-
- text = text.Replace(@"{\i1}", "<i>", StringComparison.Ordinal);
- text = text.Replace(@"{\i0}", "</i>", StringComparison.Ordinal);
- text = text.Replace(@"{\i}", "</i>", StringComparison.Ordinal);
- if (CountTagInText(text, "<i>") > CountTagInText(text, "</i>"))
- {
- text += "</i>";
- }
-
- text = text.Replace(@"{\u1}", "<u>", StringComparison.Ordinal);
- text = text.Replace(@"{\u0}", "</u>", StringComparison.Ordinal);
- text = text.Replace(@"{\u}", "</u>", StringComparison.Ordinal);
- if (CountTagInText(text, "<u>") > CountTagInText(text, "</u>"))
- {
- text += "</u>";
- }
-
- text = text.Replace(@"{\b1}", "<b>", StringComparison.Ordinal);
- text = text.Replace(@"{\b0}", "</b>", StringComparison.Ordinal);
- text = text.Replace(@"{\b}", "</b>", StringComparison.Ordinal);
- if (CountTagInText(text, "<b>") > CountTagInText(text, "</b>"))
- {
- text += "</b>";
- }
-
- return text;
- }
-
- private static bool IsInteger(string s)
- => int.TryParse(s, out _);
-
- private static int CountTagInText(string text, string tag)
- {
- int count = 0;
- int index = text.IndexOf(tag, StringComparison.Ordinal);
- while (index >= 0)
- {
- count++;
- if (index == text.Length)
- {
- return count;
- }
-
- index = text.IndexOf(tag, index + 1, StringComparison.Ordinal);
- }
-
- return count;
- }
-
- private static void CheckAndAddSubTags(ref string tagName, ref string extraTags, out bool italic)
- {
- italic = false;
- int indexOfSPlit = tagName.IndexOf('\\', StringComparison.Ordinal);
- if (indexOfSPlit > 0)
- {
- string rest = tagName.Substring(indexOfSPlit).TrimStart('\\');
- tagName = tagName.Remove(indexOfSPlit);
-
- for (int i = 0; i < 10; i++)
- {
- if (rest.StartsWith("fs", StringComparison.Ordinal) && rest.Length > 2)
- {
- indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
- string fontSize = rest;
- if (indexOfSPlit > 0)
- {
- fontSize = rest.Substring(0, indexOfSPlit);
- rest = rest.Substring(indexOfSPlit).TrimStart('\\');
- }
- else
- {
- rest = string.Empty;
- }
-
- extraTags += " size=\"" + fontSize.Substring(2) + "\"";
- }
- else if (rest.StartsWith("fn", StringComparison.Ordinal) && rest.Length > 2)
- {
- indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
- string fontName = rest;
- if (indexOfSPlit > 0)
- {
- fontName = rest.Substring(0, indexOfSPlit);
- rest = rest.Substring(indexOfSPlit).TrimStart('\\');
- }
- else
- {
- rest = string.Empty;
- }
-
- extraTags += " face=\"" + fontName.Substring(2) + "\"";
- }
- else if (rest.StartsWith("c", StringComparison.Ordinal) && rest.Length > 2)
- {
- indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
- string fontColor = rest;
- if (indexOfSPlit > 0)
- {
- fontColor = rest.Substring(0, indexOfSPlit);
- rest = rest.Substring(indexOfSPlit).TrimStart('\\');
- }
- else
- {
- rest = string.Empty;
- }
-
- string color = fontColor.Substring(2);
- color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H');
- color = color.PadLeft(6, '0');
- // switch to rrggbb from bbggrr
- color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2);
- color = color.ToLowerInvariant();
-
- extraTags += " color=\"" + color + "\"";
- }
- else if (rest.StartsWith("i1", StringComparison.Ordinal) && rest.Length > 1)
- {
- indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
- italic = true;
- if (indexOfSPlit > 0)
- {
- rest = rest.Substring(indexOfSPlit).TrimStart('\\');
- }
- else
- {
- rest = string.Empty;
- }
- }
- else if (rest.Length > 0 && rest.Contains('\\', StringComparison.Ordinal))
- {
- indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal);
- rest = rest.Substring(indexOfSPlit).TrimStart('\\');
- }
- }
- }
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
new file mode 100644
index 000000000..639a34d99
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -0,0 +1,61 @@
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
+
+namespace MediaBrowser.MediaEncoding.Subtitles
+{
+ /// <summary>
+ /// SubStation Alpha subtitle parser.
+ /// </summary>
+ /// <typeparam name="T">The <see cref="SubtitleFormat" />.</typeparam>
+ public abstract class SubtitleEditParser<T> : ISubtitleParser
+ where T : SubtitleFormat, new()
+ {
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SubtitleEditParser{T}"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ protected SubtitleEditParser(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
+ {
+ var subtitle = new Subtitle();
+ var subRip = new T();
+ var lines = stream.ReadAllLines().ToList();
+ subRip.LoadSubtitle(subtitle, lines, "untitled");
+ if (subRip.ErrorCount > 0)
+ {
+ _logger.LogError("{ErrorCount} errors encountered while parsing subtitle.");
+ }
+
+ var trackInfo = new SubtitleTrackInfo();
+ int len = subtitle.Paragraphs.Count;
+ var trackEvents = new SubtitleTrackEvent[len];
+ for (int i = 0; i < len; i++)
+ {
+ var p = subtitle.Paragraphs[i];
+ trackEvents[i] = new SubtitleTrackEvent(p.Number.ToString(CultureInfo.InvariantCulture), p.Text)
+ {
+ StartPositionTicks = p.StartTime.TimeSpan.Ticks,
+ EndPositionTicks = p.EndTime.TimeSpan.Ticks
+ };
+ }
+
+ trackInfo.TrackEvents = trackEvents;
+ return trackInfo;
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index b92c4ee06..608ebf443 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -25,9 +26,8 @@ using UtfUnknown;
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public class SubtitleEncoder : ISubtitleEncoder
+ public sealed class SubtitleEncoder : ISubtitleEncoder
{
- private readonly ILibraryManager _libraryManager;
private readonly ILogger<SubtitleEncoder> _logger;
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
@@ -42,7 +42,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
new ConcurrentDictionary<string, SemaphoreSlim>();
public SubtitleEncoder(
- ILibraryManager libraryManager,
ILogger<SubtitleEncoder> logger,
IApplicationPaths appPaths,
IFileSystem fileSystem,
@@ -50,7 +49,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
IHttpClientFactory httpClientFactory,
IMediaSourceManager mediaSourceManager)
{
- _libraryManager = libraryManager;
_logger = logger;
_appPaths = appPaths;
_fileSystem = fileSystem;
@@ -74,8 +72,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
- var reader = GetReader(inputFormat, true);
-
+ var reader = GetReader(inputFormat);
var trackInfo = reader.Parse(stream, cancellationToken);
FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
@@ -142,10 +139,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
.ConfigureAwait(false);
var inputFormat = subtitle.format;
- var writer = TryGetWriter(outputFormat);
// Return the original if we don't have any way of converting it
- if (writer == null)
+ if (!TryGetWriter(outputFormat, out var writer))
{
return subtitle.stream;
}
@@ -168,33 +164,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles
MediaStream subtitleStream,
CancellationToken cancellationToken)
{
- var inputFile = mediaSource.Path;
-
- var protocol = mediaSource.Protocol;
- if (subtitleStream.IsExternal)
- {
- protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path);
- }
+ var fileInfo = await GetReadableFile(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
- var fileInfo = await GetReadableFile(mediaSource.Path, inputFile, mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
-
- var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
+ var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false);
return (stream, fileInfo.Format);
}
- private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
+ private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
{
- if (requiresCharset)
+ if (fileInfo.IsExternal)
{
- using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
+ using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false))
{
var result = CharsetDetector.DetectFromStream(stream).Detected;
stream.Position = 0;
if (result != null)
{
- _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path);
+ _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, fileInfo.Path);
using var reader = new StreamReader(stream, result.Encoding);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
@@ -204,12 +192,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- return File.OpenRead(path);
+ return File.OpenRead(fileInfo.Path);
}
private async Task<SubtitleInfo> GetReadableFile(
- string mediaPath,
- string inputFile,
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
CancellationToken cancellationToken)
@@ -241,9 +227,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
// Extract
- var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, "." + outputFormat);
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat);
- await ExtractTextSubtitle(inputFile, mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
+ await ExtractTextSubtitle(mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
.ConfigureAwait(false);
return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false);
@@ -252,50 +238,59 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
.TrimStart('.');
- if (GetReader(currentFormat, false) == null)
+ if (TryGetReader(currentFormat, out _))
{
// Convert
- var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, ".srt");
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
}
+ if (subtitleStream.IsExternal)
+ {
+ return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
+ }
+
return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true);
}
- private ISubtitleParser GetReader(string format, bool throwIfMissing)
+ private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
{
- if (string.IsNullOrEmpty(format))
- {
- throw new ArgumentNullException(nameof(format));
- }
-
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
{
- return new SrtParser(_logger);
+ value = new SrtParser(_logger);
+ return true;
}
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
{
- return new SsaParser();
+ value = new SsaParser(_logger);
+ return true;
}
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
{
- return new AssParser();
+ value = new AssParser(_logger);
+ return true;
}
- if (throwIfMissing)
+ value = null;
+ return false;
+ }
+
+ private ISubtitleParser GetReader(string format)
+ {
+ if (TryGetReader(format, out var reader))
{
- throw new ArgumentException("Unsupported format: " + format);
+ return reader;
}
- return null;
+ throw new ArgumentException("Unsupported format: " + format);
}
- private ISubtitleWriter TryGetWriter(string format)
+ private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
{
if (string.IsNullOrEmpty(format))
{
@@ -304,32 +299,35 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
- return new JsonWriter();
+ value = new JsonWriter();
+ return true;
}
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
{
- return new SrtWriter();
+ value = new SrtWriter();
+ return true;
}
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
{
- return new VttWriter();
+ value = new VttWriter();
+ return true;
}
if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
{
- return new TtmlWriter();
+ value = new TtmlWriter();
+ return true;
}
- return null;
+ value = null;
+ return false;
}
private ISubtitleWriter GetWriter(string format)
{
- var writer = TryGetWriter(format);
-
- if (writer != null)
+ if (TryGetWriter(format, out var writer))
{
return writer;
}
@@ -399,7 +397,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentNullException(nameof(outputPath));
}
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
@@ -492,7 +490,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
_logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath);
- throw new Exception(
+ throw new FfmpegException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath));
}
@@ -504,7 +502,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <summary>
/// Extracts the text subtitle.
/// </summary>
- /// <param name="inputFile">The input file.</param>
/// <param name="mediaSource">The mediaSource.</param>
/// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
/// <param name="outputCodec">The output codec.</param>
@@ -513,7 +510,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <returns>Task.</returns>
/// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
private async Task ExtractTextSubtitle(
- string inputFile,
MediaSourceInfo mediaSource,
int subtitleStreamIndex,
string outputCodec,
@@ -529,7 +525,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!File.Exists(outputPath))
{
await ExtractTextSubtitleInternal(
- _mediaEncoder.GetInputArgument(inputFile, mediaSource),
+ _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource),
subtitleStreamIndex,
outputCodec,
outputPath,
@@ -559,7 +555,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentNullException(nameof(outputPath));
}
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
var processArgs = string.Format(
CultureInfo.InvariantCulture,
@@ -647,7 +643,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_logger.LogError(msg);
- throw new Exception(msg);
+ throw new FfmpegException(msg);
}
else
{
@@ -687,7 +683,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!string.Equals(text, newText, StringComparison.Ordinal))
{
- using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
using (var writer = new StreamWriter(fileStream, encoding))
{
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
@@ -695,15 +692,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- private string GetSubtitleCachePath(string mediaPath, MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
+ private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
{
if (mediaSource.Protocol == MediaProtocol.File)
{
var ticksParam = string.Empty;
- var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
+ var date = _fileSystem.GetLastWriteTimeUtc(mediaSource.Path);
- ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
+ ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
var prefix = filename.Slice(0, 1);
@@ -711,7 +708,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
else
{
- ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
+ ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
var prefix = filename.Slice(0, 1);
@@ -724,7 +721,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
{
- var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName;
+ var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty;
// UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal))
@@ -734,7 +731,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
charset = string.Empty;
}
- _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path);
+ _logger.LogDebug("charset {0} detected for {Path}", charset, path);
return charset;
}
diff --git a/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs b/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs
index fcc90a1f7..f9f474586 100644
--- a/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs
+++ b/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs
@@ -1,32 +1,43 @@
-#nullable disable
-#pragma warning disable CS1591
-
namespace MediaBrowser.Model.ApiClient
{
+ /// <summary>
+ /// The server discovery info model.
+ /// </summary>
public class ServerDiscoveryInfo
{
/// <summary>
- /// Gets or sets the address.
+ /// Initializes a new instance of the <see cref="ServerDiscoveryInfo"/> class.
+ /// </summary>
+ /// <param name="address">The server address.</param>
+ /// <param name="id">The server id.</param>
+ /// <param name="name">The server name.</param>
+ /// <param name="endpointAddress">The endpoint address.</param>
+ public ServerDiscoveryInfo(string address, string id, string name, string? endpointAddress = null)
+ {
+ Address = address;
+ Id = id;
+ Name = name;
+ EndpointAddress = endpointAddress;
+ }
+
+ /// <summary>
+ /// Gets the address.
/// </summary>
- /// <value>The address.</value>
- public string Address { get; set; }
+ public string Address { get; }
/// <summary>
- /// Gets or sets the server identifier.
+ /// Gets the server identifier.
/// </summary>
- /// <value>The server identifier.</value>
- public string Id { get; set; }
+ public string Id { get; }
/// <summary>
- /// Gets or sets the name.
+ /// Gets the name.
/// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>
- /// Gets or sets the endpoint address.
+ /// Gets the endpoint address.
/// </summary>
- /// <value>The endpoint address.</value>
- public string EndpointAddress { get; set; }
+ public string? EndpointAddress { get; }
}
}
diff --git a/MediaBrowser.Model/Channels/ChannelFeatures.cs b/MediaBrowser.Model/Channels/ChannelFeatures.cs
index a55754edd..d925b78b6 100644
--- a/MediaBrowser.Model/Channels/ChannelFeatures.cs
+++ b/MediaBrowser.Model/Channels/ChannelFeatures.cs
@@ -7,6 +7,13 @@ namespace MediaBrowser.Model.Channels
{
public class ChannelFeatures
{
+ public ChannelFeatures()
+ {
+ MediaTypes = Array.Empty<ChannelMediaType>();
+ ContentTypes = Array.Empty<ChannelMediaContentType>();
+ DefaultSortFields = Array.Empty<ChannelItemSortField>();
+ }
+
/// <summary>
/// Gets or sets the name.
/// </summary>
@@ -38,7 +45,7 @@ namespace MediaBrowser.Model.Channels
public ChannelMediaContentType[] ContentTypes { get; set; }
/// <summary>
- /// Represents the maximum number of records the channel allows retrieving at a time.
+ /// Gets or sets the maximum number of records the channel allows retrieving at a time.
/// </summary>
public int? MaxPageSize { get; set; }
@@ -55,7 +62,7 @@ namespace MediaBrowser.Model.Channels
public ChannelItemSortField[] DefaultSortFields { get; set; }
/// <summary>
- /// Indicates if a sort ascending/descending toggle is supported or not.
+ /// Gets or sets a value indicating whether a sort ascending/descending toggle is supported.
/// </summary>
public bool SupportsSortOrderToggle { get; set; }
@@ -76,12 +83,5 @@ namespace MediaBrowser.Model.Channels
/// </summary>
/// <value><c>true</c> if [supports content downloading]; otherwise, <c>false</c>.</value>
public bool SupportsContentDownloading { get; set; }
-
- public ChannelFeatures()
- {
- MediaTypes = Array.Empty<ChannelMediaType>();
- ContentTypes = Array.Empty<ChannelMediaContentType>();
- DefaultSortFields = Array.Empty<ChannelItemSortField>();
- }
}
}
diff --git a/MediaBrowser.Model/Channels/ChannelQuery.cs b/MediaBrowser.Model/Channels/ChannelQuery.cs
index fd90e7f06..59966127f 100644
--- a/MediaBrowser.Model/Channels/ChannelQuery.cs
+++ b/MediaBrowser.Model/Channels/ChannelQuery.cs
@@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Channels
public class ChannelQuery
{
/// <summary>
- /// Fields to return within the items, in addition to basic information.
+ /// Gets or sets the fields to return within the items, in addition to basic information.
/// </summary>
/// <value>The fields.</value>
public ItemFields[] Fields { get; set; }
@@ -28,13 +28,13 @@ namespace MediaBrowser.Model.Channels
public Guid UserId { get; set; }
/// <summary>
- /// Skips over a given number of items within the results. Use for paging.
+ /// Gets or sets the start index. Use for paging.
/// </summary>
/// <value>The start index.</value>
public int? StartIndex { get; set; }
/// <summary>
- /// The maximum number of items to return.
+ /// Gets or sets the maximum number of items to return.
/// </summary>
/// <value>The limit.</value>
public int? Limit { get; set; }
diff --git a/MediaBrowser.Model/Configuration/AccessSchedule.cs b/MediaBrowser.Model/Configuration/AccessSchedule.cs
deleted file mode 100644
index 7bd355449..000000000
--- a/MediaBrowser.Model/Configuration/AccessSchedule.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Jellyfin.Data.Enums;
-
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Configuration
-{
- public class AccessSchedule
- {
- /// <summary>
- /// Gets or sets the day of week.
- /// </summary>
- /// <value>The day of week.</value>
- public DynamicDayOfWeek DayOfWeek { get; set; }
-
- /// <summary>
- /// Gets or sets the start hour.
- /// </summary>
- /// <value>The start hour.</value>
- public double StartHour { get; set; }
-
- /// <summary>
- /// Gets or sets the end hour.
- /// </summary>
- /// <value>The end hour.</value>
- public double EndHour { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index da467e133..365bbeef6 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -5,6 +5,41 @@ namespace MediaBrowser.Model.Configuration
{
public class EncodingOptions
{
+ public EncodingOptions()
+ {
+ EnableFallbackFont = false;
+ DownMixAudioBoost = 2;
+ MaxMuxingQueueSize = 2048;
+ EnableThrottling = false;
+ ThrottleDelaySeconds = 180;
+ EncodingThreadCount = -1;
+ // This is a DRM device that is almost guaranteed to be there on every intel platform,
+ // plus it's the default one in ffmpeg if you don't specify anything
+ VaapiDevice = "/dev/dri/renderD128";
+ // This is the OpenCL device that is used for tonemapping.
+ // The left side of the dot is the platform number, and the right side is the device number on the platform.
+ OpenclDevice = "0.0";
+ EnableTonemapping = false;
+ EnableVppTonemapping = false;
+ TonemappingAlgorithm = "hable";
+ TonemappingRange = "auto";
+ TonemappingDesat = 0;
+ TonemappingThreshold = 0.8;
+ TonemappingPeak = 100;
+ TonemappingParam = 0;
+ H264Crf = 23;
+ H265Crf = 28;
+ DeinterlaceDoubleRate = false;
+ DeinterlaceMethod = "yadif";
+ EnableDecodingColorDepth10Hevc = true;
+ EnableDecodingColorDepth10Vp9 = true;
+ EnableEnhancedNvdecDecoder = true;
+ EnableHardwareEncoding = true;
+ AllowHevcEncoding = false;
+ EnableSubtitleExtraction = true;
+ HardwareDecodingCodecs = new string[] { "h264", "vc1" };
+ }
+
public int EncodingThreadCount { get; set; }
public string TranscodingTempPath { get; set; }
@@ -24,12 +59,12 @@ namespace MediaBrowser.Model.Configuration
public string HardwareAccelerationType { get; set; }
/// <summary>
- /// FFmpeg path as set by the user via the UI.
+ /// Gets or sets the FFmpeg path as set by the user via the UI.
/// </summary>
public string EncoderAppPath { get; set; }
/// <summary>
- /// The current FFmpeg path being used by the system and displayed on the transcode page.
+ /// Gets or sets the current FFmpeg path being used by the system and displayed on the transcode page.
/// </summary>
public string EncoderAppPathDisplay { get; set; }
@@ -76,40 +111,5 @@ namespace MediaBrowser.Model.Configuration
public bool EnableSubtitleExtraction { get; set; }
public string[] HardwareDecodingCodecs { get; set; }
-
- public EncodingOptions()
- {
- EnableFallbackFont = false;
- DownMixAudioBoost = 2;
- MaxMuxingQueueSize = 2048;
- EnableThrottling = false;
- ThrottleDelaySeconds = 180;
- EncodingThreadCount = -1;
- // This is a DRM device that is almost guaranteed to be there on every intel platform,
- // plus it's the default one in ffmpeg if you don't specify anything
- VaapiDevice = "/dev/dri/renderD128";
- // This is the OpenCL device that is used for tonemapping.
- // The left side of the dot is the platform number, and the right side is the device number on the platform.
- OpenclDevice = "0.0";
- EnableTonemapping = false;
- EnableVppTonemapping = false;
- TonemappingAlgorithm = "hable";
- TonemappingRange = "auto";
- TonemappingDesat = 0;
- TonemappingThreshold = 0.8;
- TonemappingPeak = 100;
- TonemappingParam = 0;
- H264Crf = 23;
- H265Crf = 28;
- DeinterlaceDoubleRate = false;
- DeinterlaceMethod = "yadif";
- EnableDecodingColorDepth10Hevc = true;
- EnableDecodingColorDepth10Vp9 = true;
- EnableEnhancedNvdecDecoder = true;
- EnableHardwareEncoding = true;
- AllowHevcEncoding = true;
- EnableSubtitleExtraction = true;
- HardwareDecodingCodecs = new string[] { "h264", "vc1" };
- }
}
}
diff --git a/MediaBrowser.Model/Configuration/ImageOption.cs b/MediaBrowser.Model/Configuration/ImageOption.cs
index 2b1268c74..0af7b7e14 100644
--- a/MediaBrowser.Model/Configuration/ImageOption.cs
+++ b/MediaBrowser.Model/Configuration/ImageOption.cs
@@ -6,6 +6,11 @@ namespace MediaBrowser.Model.Configuration
{
public class ImageOption
{
+ public ImageOption()
+ {
+ Limit = 1;
+ }
+
/// <summary>
/// Gets or sets the type.
/// </summary>
@@ -23,10 +28,5 @@ namespace MediaBrowser.Model.Configuration
/// </summary>
/// <value>The minimum width.</value>
public int MinWidth { get; set; }
-
- public ImageOption()
- {
- Limit = 1;
- }
}
}
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 77ac11d69..24698360e 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -2,13 +2,30 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Configuration
{
public class LibraryOptions
{
+ public LibraryOptions()
+ {
+ TypeOptions = Array.Empty<TypeOptions>();
+ DisabledSubtitleFetchers = Array.Empty<string>();
+ SubtitleFetcherOrder = Array.Empty<string>();
+ DisabledLocalMetadataReaders = Array.Empty<string>();
+
+ SkipSubtitlesIfAudioTrackMatches = true;
+ RequirePerfectSubtitleMatch = true;
+
+ EnablePhotos = true;
+ SaveSubtitlesWithMedia = true;
+ EnableRealtimeMonitor = true;
+ PathInfos = Array.Empty<MediaPathInfo>();
+ EnableInternetProviders = true;
+ EnableAutomaticSeriesGrouping = true;
+ SeasonZeroDisplayName = "Specials";
+ }
+
public bool EnablePhotos { get; set; }
public bool EnableRealtimeMonitor { get; set; }
@@ -79,387 +96,5 @@ namespace MediaBrowser.Model.Configuration
return null;
}
-
- public LibraryOptions()
- {
- TypeOptions = Array.Empty<TypeOptions>();
- DisabledSubtitleFetchers = Array.Empty<string>();
- SubtitleFetcherOrder = Array.Empty<string>();
- DisabledLocalMetadataReaders = Array.Empty<string>();
-
- SkipSubtitlesIfAudioTrackMatches = true;
- RequirePerfectSubtitleMatch = true;
-
- EnablePhotos = true;
- SaveSubtitlesWithMedia = true;
- EnableRealtimeMonitor = true;
- PathInfos = Array.Empty<MediaPathInfo>();
- EnableInternetProviders = true;
- EnableAutomaticSeriesGrouping = true;
- SeasonZeroDisplayName = "Specials";
- }
- }
-
- public class MediaPathInfo
- {
- public string Path { get; set; }
-
- public string NetworkPath { get; set; }
- }
-
- public class TypeOptions
- {
- public string Type { get; set; }
-
- public string[] MetadataFetchers { get; set; }
-
- public string[] MetadataFetcherOrder { get; set; }
-
- public string[] ImageFetchers { get; set; }
-
- public string[] ImageFetcherOrder { get; set; }
-
- public ImageOption[] ImageOptions { get; set; }
-
- public ImageOption GetImageOptions(ImageType type)
- {
- foreach (var i in ImageOptions)
- {
- if (i.Type == type)
- {
- return i;
- }
- }
-
- if (DefaultImageOptions.TryGetValue(Type, out ImageOption[] options))
- {
- foreach (var i in options)
- {
- if (i.Type == type)
- {
- return i;
- }
- }
- }
-
- return DefaultInstance;
- }
-
- public int GetLimit(ImageType type)
- {
- return GetImageOptions(type).Limit;
- }
-
- public int GetMinWidth(ImageType type)
- {
- return GetImageOptions(type).MinWidth;
- }
-
- public bool IsEnabled(ImageType type)
- {
- return GetLimit(type) > 0;
- }
-
- public TypeOptions()
- {
- MetadataFetchers = Array.Empty<string>();
- MetadataFetcherOrder = Array.Empty<string>();
- ImageFetchers = Array.Empty<string>();
- ImageFetcherOrder = Array.Empty<string>();
- ImageOptions = Array.Empty<ImageOption>();
- }
-
- public static Dictionary<string, ImageOption[]> DefaultImageOptions = new Dictionary<string, ImageOption[]>
- {
- {
- "Movie", new []
- {
- new ImageOption
- {
- Limit = 1,
- MinWidth = 1280,
- Type = ImageType.Backdrop
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Art
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Disc
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Primary
- },
-
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Banner
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Thumb
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Logo
- }
- }
- },
- {
- "MusicVideo", new []
- {
- new ImageOption
- {
- Limit = 1,
- MinWidth = 1280,
- Type = ImageType.Backdrop
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Art
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Disc
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Primary
- },
-
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Banner
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Thumb
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Logo
- }
- }
- },
- {
- "Series", new []
- {
- new ImageOption
- {
- Limit = 1,
- MinWidth = 1280,
- Type = ImageType.Backdrop
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Art
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Primary
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Banner
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Thumb
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Logo
- }
- }
- },
- {
- "MusicAlbum", new []
- {
- new ImageOption
- {
- Limit = 0,
- MinWidth = 1280,
- Type = ImageType.Backdrop
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Disc
- }
- }
- },
- {
- "MusicArtist", new []
- {
- new ImageOption
- {
- Limit = 1,
- MinWidth = 1280,
- Type = ImageType.Backdrop
- },
-
- // Don't download this by default
- // They do look great, but most artists won't have them, which means a banner view isn't really possible
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Banner
- },
-
- // Don't download this by default
- // Generally not used
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Art
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Logo
- }
- }
- },
- {
- "BoxSet", new []
- {
- new ImageOption
- {
- Limit = 1,
- MinWidth = 1280,
- Type = ImageType.Backdrop
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Primary
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Thumb
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Logo
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Art
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Disc
- },
-
- // Don't download this by default as it's rarely used.
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Banner
- }
- }
- },
- {
- "Season", new []
- {
- new ImageOption
- {
- Limit = 0,
- MinWidth = 1280,
- Type = ImageType.Backdrop
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Primary
- },
-
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Banner
- },
-
- new ImageOption
- {
- Limit = 0,
- Type = ImageType.Thumb
- }
- }
- },
- {
- "Episode", new []
- {
- new ImageOption
- {
- Limit = 0,
- MinWidth = 1280,
- Type = ImageType.Backdrop
- },
-
- new ImageOption
- {
- Limit = 1,
- Type = ImageType.Primary
- }
- }
- }
- };
-
- public static ImageOption DefaultInstance = new ImageOption();
}
}
diff --git a/MediaBrowser.Model/Configuration/MediaPathInfo.cs b/MediaBrowser.Model/Configuration/MediaPathInfo.cs
new file mode 100644
index 000000000..4f311c58f
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/MediaPathInfo.cs
@@ -0,0 +1,12 @@
+#nullable disable
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class MediaPathInfo
+ {
+ public string Path { get; set; }
+
+ public string NetworkPath { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/MetadataConfiguration.cs b/MediaBrowser.Model/Configuration/MetadataConfiguration.cs
index 706831bdd..be044243d 100644
--- a/MediaBrowser.Model/Configuration/MetadataConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/MetadataConfiguration.cs
@@ -4,11 +4,11 @@ namespace MediaBrowser.Model.Configuration
{
public class MetadataConfiguration
{
- public bool UseFileCreationTimeForDateAdded { get; set; }
-
public MetadataConfiguration()
{
UseFileCreationTimeForDateAdded = true;
}
+
+ public bool UseFileCreationTimeForDateAdded { get; set; }
}
}
diff --git a/MediaBrowser.Model/Configuration/MetadataOptions.cs b/MediaBrowser.Model/Configuration/MetadataOptions.cs
index e7dc3da3c..76b72bd08 100644
--- a/MediaBrowser.Model/Configuration/MetadataOptions.cs
+++ b/MediaBrowser.Model/Configuration/MetadataOptions.cs
@@ -10,6 +10,16 @@ namespace MediaBrowser.Model.Configuration
/// </summary>
public class MetadataOptions
{
+ public MetadataOptions()
+ {
+ DisabledMetadataSavers = Array.Empty<string>();
+ LocalMetadataReaderOrder = Array.Empty<string>();
+ DisabledMetadataFetchers = Array.Empty<string>();
+ MetadataFetcherOrder = Array.Empty<string>();
+ DisabledImageFetchers = Array.Empty<string>();
+ ImageFetcherOrder = Array.Empty<string>();
+ }
+
public string ItemType { get; set; }
public string[] DisabledMetadataSavers { get; set; }
@@ -23,15 +33,5 @@ namespace MediaBrowser.Model.Configuration
public string[] DisabledImageFetchers { get; set; }
public string[] ImageFetcherOrder { get; set; }
-
- public MetadataOptions()
- {
- DisabledMetadataSavers = Array.Empty<string>();
- LocalMetadataReaderOrder = Array.Empty<string>();
- DisabledMetadataFetchers = Array.Empty<string>();
- MetadataFetcherOrder = Array.Empty<string>();
- DisabledImageFetchers = Array.Empty<string>();
- ImageFetcherOrder = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs b/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs
index 0c197ee02..aa07d6623 100644
--- a/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs
+++ b/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs
@@ -8,6 +8,12 @@ namespace MediaBrowser.Model.Configuration
{
public class MetadataPluginSummary
{
+ public MetadataPluginSummary()
+ {
+ SupportedImageTypes = Array.Empty<ImageType>();
+ Plugins = Array.Empty<MetadataPlugin>();
+ }
+
/// <summary>
/// Gets or sets the type of the item.
/// </summary>
@@ -25,11 +31,5 @@ namespace MediaBrowser.Model.Configuration
/// </summary>
/// <value>The supported image types.</value>
public ImageType[] SupportedImageTypes { get; set; }
-
- public MetadataPluginSummary()
- {
- SupportedImageTypes = Array.Empty<ImageType>();
- Plugins = Array.Empty<MetadataPlugin>();
- }
}
}
diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs
index bffaa8594..2c9b5f005 100644
--- a/MediaBrowser.Model/Configuration/PathSubstitution.cs
+++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
namespace MediaBrowser.Model.Configuration
{
/// <summary>
diff --git a/MediaBrowser.Model/Configuration/TypeOptions.cs b/MediaBrowser.Model/Configuration/TypeOptions.cs
new file mode 100644
index 000000000..d0179e5aa
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/TypeOptions.cs
@@ -0,0 +1,365 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class TypeOptions
+ {
+ public static readonly ImageOption DefaultInstance = new ImageOption();
+
+ public static readonly Dictionary<string, ImageOption[]> DefaultImageOptions = new Dictionary<string, ImageOption[]>
+ {
+ {
+ "Movie", new[]
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Disc
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Thumb
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ }
+ }
+ },
+ {
+ "MusicVideo", new[]
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Disc
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Thumb
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ }
+ }
+ },
+ {
+ "Series", new[]
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Banner
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Thumb
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ }
+ }
+ },
+ {
+ "MusicAlbum", new[]
+ {
+ new ImageOption
+ {
+ Limit = 0,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Disc
+ }
+ }
+ },
+ {
+ "MusicArtist", new[]
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default
+ // They do look great, but most artists won't have them, which means a banner view isn't really possible
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ },
+
+ // Don't download this by default
+ // Generally not used
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ }
+ }
+ },
+ {
+ "BoxSet", new[]
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Thumb
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Disc
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ }
+ }
+ },
+ {
+ "Season", new[]
+ {
+ new ImageOption
+ {
+ Limit = 0,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ },
+
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Thumb
+ }
+ }
+ },
+ {
+ "Episode", new[]
+ {
+ new ImageOption
+ {
+ Limit = 0,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ }
+ }
+ }
+ };
+
+ public TypeOptions()
+ {
+ MetadataFetchers = Array.Empty<string>();
+ MetadataFetcherOrder = Array.Empty<string>();
+ ImageFetchers = Array.Empty<string>();
+ ImageFetcherOrder = Array.Empty<string>();
+ ImageOptions = Array.Empty<ImageOption>();
+ }
+
+ public string Type { get; set; }
+
+ public string[] MetadataFetchers { get; set; }
+
+ public string[] MetadataFetcherOrder { get; set; }
+
+ public string[] ImageFetchers { get; set; }
+
+ public string[] ImageFetcherOrder { get; set; }
+
+ public ImageOption[] ImageOptions { get; set; }
+
+ public ImageOption GetImageOptions(ImageType type)
+ {
+ foreach (var i in ImageOptions)
+ {
+ if (i.Type == type)
+ {
+ return i;
+ }
+ }
+
+ if (DefaultImageOptions.TryGetValue(Type, out ImageOption[] options))
+ {
+ foreach (var i in options)
+ {
+ if (i.Type == type)
+ {
+ return i;
+ }
+ }
+ }
+
+ return DefaultInstance;
+ }
+
+ public int GetLimit(ImageType type)
+ {
+ return GetImageOptions(type).Limit;
+ }
+
+ public int GetMinWidth(ImageType type)
+ {
+ return GetImageOptions(type).MinWidth;
+ }
+
+ public bool IsEnabled(ImageType type)
+ {
+ return GetLimit(type) > 0;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs
index cc0e0c468..935e6cbe1 100644
--- a/MediaBrowser.Model/Configuration/UserConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs
@@ -12,6 +12,24 @@ namespace MediaBrowser.Model.Configuration
public class UserConfiguration
{
/// <summary>
+ /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
+ /// </summary>
+ public UserConfiguration()
+ {
+ EnableNextEpisodeAutoPlay = true;
+ RememberAudioSelections = true;
+ RememberSubtitleSelections = true;
+
+ HidePlayedInLatest = true;
+ PlayDefaultAudioTrack = true;
+
+ LatestItemsExcludes = Array.Empty<string>();
+ OrderedViews = Array.Empty<string>();
+ MyMediaExcludes = Array.Empty<string>();
+ GroupedFolders = Array.Empty<string>();
+ }
+
+ /// <summary>
/// Gets or sets the audio language preference.
/// </summary>
/// <value>The audio language preference.</value>
@@ -52,23 +70,5 @@ namespace MediaBrowser.Model.Configuration
public bool RememberSubtitleSelections { get; set; }
public bool EnableNextEpisodeAutoPlay { get; set; }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
- /// </summary>
- public UserConfiguration()
- {
- EnableNextEpisodeAutoPlay = true;
- RememberAudioSelections = true;
- RememberSubtitleSelections = true;
-
- HidePlayedInLatest = true;
- PlayDefaultAudioTrack = true;
-
- LatestItemsExcludes = Array.Empty<string>();
- OrderedViews = Array.Empty<string>();
- MyMediaExcludes = Array.Empty<string>();
- GroupedFolders = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
index 4d5f996f8..8ad070dcb 100644
--- a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
+++ b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
@@ -5,6 +5,14 @@ namespace MediaBrowser.Model.Configuration
{
public class XbmcMetadataOptions
{
+ public XbmcMetadataOptions()
+ {
+ ReleaseDateFormat = "yyyy-MM-dd";
+
+ SaveImagePathsInNfo = true;
+ EnablePathSubstitution = true;
+ }
+
public string UserId { get; set; }
public string ReleaseDateFormat { get; set; }
@@ -14,13 +22,5 @@ namespace MediaBrowser.Model.Configuration
public bool EnablePathSubstitution { get; set; }
public bool EnableExtraThumbsDuplication { get; set; }
-
- public XbmcMetadataOptions()
- {
- ReleaseDateFormat = "yyyy-MM-dd";
-
- SaveImagePathsInNfo = true;
- EnablePathSubstitution = true;
- }
}
}
diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs
index bbb8bf426..4d4d8d78c 100644
--- a/MediaBrowser.Model/Dlna/AudioOptions.cs
+++ b/MediaBrowser.Model/Dlna/AudioOptions.cs
@@ -34,20 +34,20 @@ namespace MediaBrowser.Model.Dlna
public DeviceProfile Profile { get; set; }
/// <summary>
- /// Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
+ /// Gets or sets a media source id. Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
/// </summary>
public string MediaSourceId { get; set; }
public string DeviceId { get; set; }
/// <summary>
- /// Allows an override of supported number of audio channels
- /// Example: DeviceProfile supports five channel, but user only has stereo speakers
+ /// Gets or sets an override of supported number of audio channels
+ /// Example: DeviceProfile supports five channel, but user only has stereo speakers.
/// </summary>
public int? MaxAudioChannels { get; set; }
/// <summary>
- /// The application's configured quality setting.
+ /// Gets or sets the application's configured quality setting.
/// </summary>
public int? MaxBitrate { get; set; }
@@ -66,6 +66,7 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// Gets the maximum bitrate.
/// </summary>
+ /// <param name="isAudio">Whether or not this is audio.</param>
/// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
public int? GetMaxBitrate(bool isAudio)
{
diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs
index d4fd3e673..8343cf028 100644
--- a/MediaBrowser.Model/Dlna/CodecProfile.cs
+++ b/MediaBrowser.Model/Dlna/CodecProfile.cs
@@ -9,6 +9,12 @@ namespace MediaBrowser.Model.Dlna
{
public class CodecProfile
{
+ public CodecProfile()
+ {
+ Conditions = Array.Empty<ProfileCondition>();
+ ApplyConditions = Array.Empty<ProfileCondition>();
+ }
+
[XmlAttribute("type")]
public CodecType Type { get; set; }
@@ -22,12 +28,6 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("container")]
public string Container { get; set; }
- public CodecProfile()
- {
- Conditions = Array.Empty<ProfileCondition>();
- ApplyConditions = Array.Empty<ProfileCondition>();
- }
-
public string[] GetCodecs()
{
return ContainerProfile.SplitValue(Codec);
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index faf1ee41b..55c4dd074 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System;
-using System.Linq;
using System.Globalization;
+using System.Linq;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs
index 56c89d854..740966088 100644
--- a/MediaBrowser.Model/Dlna/ContainerProfile.cs
+++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -12,22 +11,12 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
- public ProfileCondition[] Conditions { get; set; }
+ public ProfileCondition[]? Conditions { get; set; } = Array.Empty<ProfileCondition>();
[XmlAttribute("container")]
- public string Container { get; set; }
+ public string Container { get; set; } = string.Empty;
- public ContainerProfile()
- {
- Conditions = Array.Empty<ProfileCondition>();
- }
-
- public string[] GetContainers()
- {
- return SplitValue(Container);
- }
-
- public static string[] SplitValue(string value)
+ public static string[] SplitValue(string? value)
{
if (string.IsNullOrEmpty(value))
{
@@ -37,14 +26,14 @@ namespace MediaBrowser.Model.Dlna
return value.Split(',', StringSplitOptions.RemoveEmptyEntries);
}
- public bool ContainsContainer(string container)
+ public bool ContainsContainer(string? container)
{
- var containers = GetContainers();
+ var containers = SplitValue(Container);
return ContainsContainer(containers, container);
}
- public static bool ContainsContainer(string profileContainers, string inputContainer)
+ public static bool ContainsContainer(string? profileContainers, string? inputContainer)
{
var isNegativeList = false;
if (profileContainers != null && profileContainers.StartsWith('-'))
@@ -56,46 +45,30 @@ namespace MediaBrowser.Model.Dlna
return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer);
}
- public static bool ContainsContainer(string[] profileContainers, string inputContainer)
+ public static bool ContainsContainer(string[]? profileContainers, string? inputContainer)
{
return ContainsContainer(profileContainers, false, inputContainer);
}
- public static bool ContainsContainer(string[] profileContainers, bool isNegativeList, string inputContainer)
+ public static bool ContainsContainer(string[]? profileContainers, bool isNegativeList, string? inputContainer)
{
- if (profileContainers.Length == 0)
+ if (profileContainers == null || profileContainers.Length == 0)
{
+ // Empty profiles always support all containers/codecs
return true;
}
- if (isNegativeList)
- {
- var allInputContainers = SplitValue(inputContainer);
+ var allInputContainers = SplitValue(inputContainer);
- foreach (var container in allInputContainers)
- {
- if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
- }
-
- return true;
- }
- else
+ foreach (var container in allInputContainers)
{
- var allInputContainers = SplitValue(inputContainer);
-
- foreach (var container in allInputContainers)
+ if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase))
{
- if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase))
- {
- return true;
- }
+ return !isNegativeList;
}
-
- return false;
}
+
+ return isNegativeList;
}
}
}
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 50e3374f7..600a44157 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -10,14 +10,8 @@ namespace MediaBrowser.Model.Dlna
{
public class ContentFeatureBuilder
{
- private readonly DeviceProfile _profile;
-
- public ContentFeatureBuilder(DeviceProfile profile)
- {
- _profile = profile;
- }
-
- public string BuildImageHeader(
+ public static string BuildImageHeader(
+ DeviceProfile profile,
string container,
int? width,
int? height,
@@ -38,27 +32,31 @@ namespace MediaBrowser.Model.Dlna
";DLNA.ORG_FLAGS={0}",
DlnaMaps.FlagsToString(flagValue));
- ResponseProfile mediaProfile = _profile.GetImageMediaProfile(
- container,
- width,
- height);
-
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))
{
- orgPn = GetImageOrgPnValue(container, width, height);
+ return orgOp.TrimStart(';') + orgCi + dlnaflags;
}
- string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn;
-
- return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
+ return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags;
}
- public string BuildAudioHeader(
+ public static string BuildAudioHeader(
+ DeviceProfile profile,
string container,
string audioCodec,
int? audioBitrate,
@@ -81,20 +79,20 @@ namespace MediaBrowser.Model.Dlna
DlnaFlags.DlnaV15;
// if (isDirectStream)
- //{
- // flagValue = flagValue | DlnaFlags.ByteBasedSeek;
- //}
- // else if (runtimeTicks.HasValue)
- //{
- // flagValue = flagValue | DlnaFlags.TimeBasedSeek;
- //}
+ // {
+ // 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(
+ ResponseProfile mediaProfile = profile.GetAudioMediaProfile(
container,
audioCodec,
audioChannels,
@@ -109,12 +107,16 @@ namespace MediaBrowser.Model.Dlna
orgPn = GetAudioOrgPnValue(container, audioBitrate, audioSampleRate, audioChannels);
}
- string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn;
+ if (string.IsNullOrEmpty(orgPn))
+ {
+ return orgOp.TrimStart(';') + orgCi + dlnaflags;
+ }
- return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
+ return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags;
}
- public List<string> BuildVideoHeader(
+ public static List<string> BuildVideoHeader(
+ DeviceProfile profile,
string container,
string videoCodec,
string audioCodec,
@@ -150,18 +152,20 @@ namespace MediaBrowser.Model.Dlna
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.GetVideoMediaProfile(
+ // {
+ // 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.GetVideoMediaProfile(
container,
audioCodec,
videoCodec,
@@ -190,9 +194,9 @@ namespace MediaBrowser.Model.Dlna
}
else
{
- foreach (string s in GetVideoOrgPnValue(container, videoCodec, audioCodec, width, height, timestamp))
+ foreach (var s in GetVideoOrgPnValue(container, videoCodec, audioCodec, width, height, timestamp))
{
- orgPnValues.Add(s);
+ orgPnValues.Add(s.ToString());
break;
}
}
@@ -201,20 +205,20 @@ namespace MediaBrowser.Model.Dlna
foreach (string orgPn in orgPnValues)
{
- string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn;
-
- var value = (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
-
- contentFeatureList.Add(value);
+ if (string.IsNullOrEmpty(orgPn))
+ {
+ contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags);
+ continue;
+ }
+ else
+ {
+ contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgCi + dlnaflags);
+ }
}
if (orgPnValues.Count == 0)
{
- string contentFeatures = string.Empty;
-
- var value = (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
-
- contentFeatureList.Add(value);
+ contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags);
}
return contentFeatureList;
@@ -222,19 +226,14 @@ namespace MediaBrowser.Model.Dlna
private static string GetImageOrgPnValue(string container, int? width, int? height)
{
- MediaFormatProfile? format = new MediaFormatProfileResolver()
- .ResolveImageFormat(
- container,
- width,
- 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 = new MediaFormatProfileResolver()
- .ResolveAudioFormat(
+ MediaFormatProfile? format = MediaFormatProfileResolver.ResolveAudioFormat(
container,
audioBitrate,
audioSampleRate,
@@ -243,9 +242,9 @@ namespace MediaBrowser.Model.Dlna
return format.HasValue ? format.Value.ToString() : null;
}
- private static string[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp)
+ private static MediaFormatProfile[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp)
{
- return new MediaFormatProfileResolver().ResolveVideoFormat(container, videoCodec, audioCodec, width, height, 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 ff5186658..feb3d880e 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -1,6 +1,6 @@
-#nullable disable
#pragma warning disable CA1819 // Properties should not return arrays
using System;
+using System.ComponentModel;
using System.Linq;
using System.Xml.Serialization;
using MediaBrowser.Model.MediaInfo;
@@ -8,226 +8,219 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
{
/// <summary>
- /// Defines the <see cref="DeviceProfile" />.
+ /// A <see cref="DeviceProfile" /> represents a set of metadata which determines which content a certain device is able to play.
+ /// <br/>
+ /// Specifically, it defines the supported <see cref="ContainerProfiles">containers</see> and
+ /// <see cref="CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
+ /// the device is able to direct play (without transcoding or remuxing),
+ /// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.
/// </summary>
[XmlRoot("Profile")]
public class DeviceProfile
{
/// <summary>
- /// Initializes a new instance of the <see cref="DeviceProfile"/> class.
+ /// Gets or sets the name of this device profile.
/// </summary>
- public DeviceProfile()
- {
- DirectPlayProfiles = Array.Empty<DirectPlayProfile>();
- TranscodingProfiles = Array.Empty<TranscodingProfile>();
- ResponseProfiles = Array.Empty<ResponseProfile>();
- CodecProfiles = Array.Empty<CodecProfile>();
- ContainerProfiles = Array.Empty<ContainerProfile>();
- SubtitleProfiles = Array.Empty<SubtitleProfile>();
-
- XmlRootAttributes = Array.Empty<XmlAttribute>();
-
- SupportedMediaTypes = "Audio,Photo,Video";
- MaxStreamingBitrate = 8000000;
- MaxStaticBitrate = 8000000;
- MusicStreamingTranscodingBitrate = 128000;
- }
-
- /// <summary>
- /// Gets or sets the Name.
- /// </summary>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the Id.
/// </summary>
[XmlIgnore]
- public string Id { get; set; }
+ public string? Id { get; set; }
/// <summary>
/// Gets or sets the Identification.
/// </summary>
- public DeviceIdentification Identification { get; set; }
+ public DeviceIdentification? Identification { get; set; }
/// <summary>
- /// Gets or sets the FriendlyName.
+ /// Gets or sets the friendly name of the device profile, which can be shown to users.
/// </summary>
- public string FriendlyName { get; set; }
+ public string? FriendlyName { get; set; }
/// <summary>
- /// Gets or sets the Manufacturer.
+ /// Gets or sets the manufacturer of the device which this profile represents.
/// </summary>
- public string Manufacturer { get; set; }
+ public string? Manufacturer { get; set; }
/// <summary>
- /// Gets or sets the ManufacturerUrl.
+ /// Gets or sets an url for the manufacturer of the device which this profile represents.
/// </summary>
- public string ManufacturerUrl { get; set; }
+ public string? ManufacturerUrl { get; set; }
/// <summary>
- /// Gets or sets the ModelName.
+ /// Gets or sets the model name of the device which this profile represents.
/// </summary>
- public string ModelName { get; set; }
+ public string? ModelName { get; set; }
/// <summary>
- /// Gets or sets the ModelDescription.
+ /// Gets or sets the model description of the device which this profile represents.
/// </summary>
- public string ModelDescription { get; set; }
+ public string? ModelDescription { get; set; }
/// <summary>
- /// Gets or sets the ModelNumber.
+ /// Gets or sets the model number of the device which this profile represents.
/// </summary>
- public string ModelNumber { get; set; }
+ public string? ModelNumber { get; set; }
/// <summary>
/// Gets or sets the ModelUrl.
/// </summary>
- public string ModelUrl { get; set; }
+ public string? ModelUrl { get; set; }
/// <summary>
- /// Gets or sets the SerialNumber.
+ /// Gets or sets the serial number of the device which this profile represents.
/// </summary>
- public string SerialNumber { get; set; }
+ public string? SerialNumber { get; set; }
/// <summary>
/// Gets or sets a value indicating whether EnableAlbumArtInDidl.
/// </summary>
+ [DefaultValue(false)]
public bool EnableAlbumArtInDidl { get; set; }
/// <summary>
/// Gets or sets a value indicating whether EnableSingleAlbumArtLimit.
/// </summary>
+ [DefaultValue(false)]
public bool EnableSingleAlbumArtLimit { get; set; }
/// <summary>
/// Gets or sets a value indicating whether EnableSingleSubtitleLimit.
/// </summary>
+ [DefaultValue(false)]
public bool EnableSingleSubtitleLimit { get; set; }
/// <summary>
/// Gets or sets the SupportedMediaTypes.
/// </summary>
- public string SupportedMediaTypes { get; set; }
+ public string SupportedMediaTypes { get; set; } = "Audio,Photo,Video";
/// <summary>
/// Gets or sets the UserId.
/// </summary>
- public string UserId { get; set; }
+ public string? UserId { get; set; }
/// <summary>
/// Gets or sets the AlbumArtPn.
/// </summary>
- public string AlbumArtPn { get; set; }
+ public string? AlbumArtPn { get; set; }
/// <summary>
/// Gets or sets the MaxAlbumArtWidth.
/// </summary>
- public int MaxAlbumArtWidth { get; set; }
+ public int? MaxAlbumArtWidth { get; set; }
/// <summary>
/// Gets or sets the MaxAlbumArtHeight.
/// </summary>
- public int MaxAlbumArtHeight { get; set; }
+ public int? MaxAlbumArtHeight { get; set; }
/// <summary>
- /// Gets or sets the MaxIconWidth.
+ /// Gets or sets the maximum allowed width of embedded icons.
/// </summary>
public int? MaxIconWidth { get; set; }
/// <summary>
- /// Gets or sets the MaxIconHeight.
+ /// Gets or sets the maximum allowed height of embedded icons.
/// </summary>
public int? MaxIconHeight { get; set; }
/// <summary>
- /// Gets or sets the MaxStreamingBitrate.
+ /// Gets or sets the maximum allowed bitrate for all streamed content.
/// </summary>
- public int? MaxStreamingBitrate { get; set; }
+ public int? MaxStreamingBitrate { get; set; } = 8000000;
/// <summary>
- /// Gets or sets the MaxStaticBitrate.
+ /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files).
/// </summary>
- public int? MaxStaticBitrate { get; set; }
+ public int? MaxStaticBitrate { get; set; } = 8000000;
/// <summary>
- /// Gets or sets the MusicStreamingTranscodingBitrate.
+ /// Gets or sets the maximum allowed bitrate for transcoded music streams.
/// </summary>
- public int? MusicStreamingTranscodingBitrate { get; set; }
+ public int? MusicStreamingTranscodingBitrate { get; set; } = 128000;
/// <summary>
- /// Gets or sets the MaxStaticMusicBitrate.
+ /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files.
/// </summary>
- public int? MaxStaticMusicBitrate { get; set; }
+ public int? MaxStaticMusicBitrate { get; set; } = 8000000;
/// <summary>
/// Gets or sets the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace.
/// </summary>
- public string SonyAggregationFlags { get; set; }
+ public string? SonyAggregationFlags { get; set; }
/// <summary>
/// Gets or sets the ProtocolInfo.
/// </summary>
- public string ProtocolInfo { get; set; }
+ public string? ProtocolInfo { get; set; }
/// <summary>
/// Gets or sets the TimelineOffsetSeconds.
/// </summary>
+ [DefaultValue(0)]
public int TimelineOffsetSeconds { get; set; }
/// <summary>
/// Gets or sets a value indicating whether RequiresPlainVideoItems.
/// </summary>
+ [DefaultValue(false)]
public bool RequiresPlainVideoItems { get; set; }
/// <summary>
/// Gets or sets a value indicating whether RequiresPlainFolders.
/// </summary>
+ [DefaultValue(false)]
public bool RequiresPlainFolders { get; set; }
/// <summary>
/// Gets or sets a value indicating whether EnableMSMediaReceiverRegistrar.
/// </summary>
+ [DefaultValue(false)]
public bool EnableMSMediaReceiverRegistrar { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IgnoreTranscodeByteRangeRequests.
/// </summary>
+ [DefaultValue(false)]
public bool IgnoreTranscodeByteRangeRequests { get; set; }
/// <summary>
/// Gets or sets the XmlRootAttributes.
/// </summary>
- public XmlAttribute[] XmlRootAttributes { get; set; }
+ public XmlAttribute[] XmlRootAttributes { get; set; } = Array.Empty<XmlAttribute>();
/// <summary>
/// Gets or sets the direct play profiles.
/// </summary>
- public DirectPlayProfile[] DirectPlayProfiles { get; set; }
+ public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty<DirectPlayProfile>();
/// <summary>
/// Gets or sets the transcoding profiles.
/// </summary>
- public TranscodingProfile[] TranscodingProfiles { get; set; }
+ public TranscodingProfile[] TranscodingProfiles { get; set; } = Array.Empty<TranscodingProfile>();
/// <summary>
- /// Gets or sets the ContainerProfiles.
+ /// Gets or sets the container profiles.
/// </summary>
- public ContainerProfile[] ContainerProfiles { get; set; }
+ public ContainerProfile[] ContainerProfiles { get; set; } = Array.Empty<ContainerProfile>();
/// <summary>
- /// Gets or sets the CodecProfiles.
+ /// Gets or sets the codec profiles.
/// </summary>
- public CodecProfile[] CodecProfiles { get; set; }
+ public CodecProfile[] CodecProfiles { get; set; } = Array.Empty<CodecProfile>();
/// <summary>
/// Gets or sets the ResponseProfiles.
/// </summary>
- public ResponseProfile[] ResponseProfiles { get; set; }
+ public ResponseProfile[] ResponseProfiles { get; set; } = Array.Empty<ResponseProfile>();
/// <summary>
- /// Gets or sets the SubtitleProfiles.
+ /// Gets or sets the subtitle profiles.
/// </summary>
- public SubtitleProfile[] SubtitleProfiles { get; set; }
+ public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty<SubtitleProfile>();
/// <summary>
/// The GetSupportedMediaTypes.
@@ -244,13 +237,13 @@ namespace MediaBrowser.Model.Dlna
/// <param name="container">The container.</param>
/// <param name="audioCodec">The audio Codec.</param>
/// <returns>A <see cref="TranscodingProfile"/>.</returns>
- public TranscodingProfile GetAudioTranscodingProfile(string container, string audioCodec)
+ public TranscodingProfile? GetAudioTranscodingProfile(string? container, string? audioCodec)
{
container = (container ?? string.Empty).TrimStart('.');
foreach (var i in TranscodingProfiles)
{
- if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Audio)
+ if (i.Type != DlnaProfileType.Audio)
{
continue;
}
@@ -278,13 +271,13 @@ namespace MediaBrowser.Model.Dlna
/// <param name="audioCodec">The audio Codec.</param>
/// <param name="videoCodec">The video Codec.</param>
/// <returns>The <see cref="TranscodingProfile"/>.</returns>
- public TranscodingProfile GetVideoTranscodingProfile(string container, string audioCodec, string videoCodec)
+ public TranscodingProfile? GetVideoTranscodingProfile(string? container, string? audioCodec, string? videoCodec)
{
container = (container ?? string.Empty).TrimStart('.');
foreach (var i in TranscodingProfiles)
{
- if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Video)
+ if (i.Type != DlnaProfileType.Video)
{
continue;
}
@@ -299,7 +292,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!string.Equals(videoCodec, i.VideoCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(videoCodec, i.VideoCodec, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -320,7 +313,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="audioSampleRate">The audio sample rate.</param>
/// <param name="audioBitDepth">The audio bit depth.</param>
/// <returns>The <see cref="ResponseProfile"/>.</returns>
- public ResponseProfile GetAudioMediaProfile(string container, string audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
+ public ResponseProfile? GetAudioMediaProfile(string container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
{
foreach (var i in ResponseProfiles)
{
@@ -384,7 +377,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns>The <see cref="ResponseProfile"/>.</returns>
- public ResponseProfile GetImageMediaProfile(string container, int? width, int? height)
+ public ResponseProfile? GetImageMediaProfile(string container, int? width, int? height)
{
foreach (var i in ResponseProfiles)
{
@@ -442,10 +435,10 @@ namespace MediaBrowser.Model.Dlna
/// <param name="videoCodecTag">The video Codec tag.</param>
/// <param name="isAvc">True if Avc.</param>
/// <returns>The <see cref="ResponseProfile"/>.</returns>
- public ResponseProfile GetVideoMediaProfile(
+ public ResponseProfile? GetVideoMediaProfile(
string container,
- string audioCodec,
- string videoCodec,
+ string? audioCodec,
+ string? videoCodec,
int? width,
int? height,
int? bitDepth,
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
index 88cb83991..fa3ad098f 100644
--- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -1,6 +1,6 @@
-#nullable disable
#pragma warning disable CS1591
+using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
namespace MediaBrowser.Model.Dlna
@@ -8,13 +8,13 @@ namespace MediaBrowser.Model.Dlna
public class DirectPlayProfile
{
[XmlAttribute("container")]
- public string Container { get; set; }
+ public string? Container { get; set; }
[XmlAttribute("audioCodec")]
- public string AudioCodec { get; set; }
+ public string? AudioCodec { get; set; }
[XmlAttribute("videoCodec")]
- public string VideoCodec { get; set; }
+ public string? VideoCodec { get; set; }
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
diff --git a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
index 05209e53d..086088dea 100644
--- a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
+++ b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
@@ -8,6 +8,7 @@ namespace MediaBrowser.Model.Dlna
public interface IDeviceDiscovery
{
event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered;
+
event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
}
}
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
index 3c955989a..7ce248509 100644
--- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
+++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
@@ -9,16 +9,9 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
{
- public class MediaFormatProfileResolver
+ public static class MediaFormatProfileResolver
{
- public string[] ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
- {
- return ResolveVideoFormatInternal(container, videoCodec, audioCodec, width, height, timestampType)
- .Select(i => i.ToString())
- .ToArray();
- }
-
- private MediaFormatProfile[] ResolveVideoFormatInternal(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
+ public static MediaFormatProfile[] ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
{
if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase))
{
@@ -57,7 +50,6 @@ namespace MediaBrowser.Model.Dlna
string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) ||
string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase))
{
-
return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType);
}
@@ -85,7 +77,7 @@ namespace MediaBrowser.Model.Dlna
return Array.Empty<MediaFormatProfile>();
}
- private MediaFormatProfile[] ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
+ private static MediaFormatProfile[] ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
{
string suffix = string.Empty;
@@ -210,12 +202,12 @@ namespace MediaBrowser.Model.Dlna
return Array.Empty<MediaFormatProfile>();
}
- private MediaFormatProfile ValueOf(string value)
+ private static MediaFormatProfile ValueOf(string value)
{
return (MediaFormatProfile)Enum.Parse(typeof(MediaFormatProfile), value, true);
}
- private MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height)
+ private static MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height)
{
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
@@ -288,7 +280,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- private MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec)
+ private static MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec)
{
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
@@ -318,12 +310,11 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- private MediaFormatProfile? ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height)
+ 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))
@@ -373,7 +364,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- public MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels)
+ public static MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels)
{
if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase))
{
@@ -415,7 +406,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- private MediaFormatProfile ResolveAudioASFFormat(int? bitrate)
+ private static MediaFormatProfile ResolveAudioASFFormat(int? bitrate)
{
if (bitrate.HasValue && bitrate.Value <= 193)
{
@@ -425,7 +416,7 @@ namespace MediaBrowser.Model.Dlna
return MediaFormatProfile.WMA_FULL;
}
- private MediaFormatProfile? ResolveAudioLPCMFormat(int? frequency, int? channels)
+ private static MediaFormatProfile? ResolveAudioLPCMFormat(int? frequency, int? channels)
{
if (frequency.HasValue && channels.HasValue)
{
@@ -455,7 +446,7 @@ namespace MediaBrowser.Model.Dlna
return MediaFormatProfile.LPCM16_48_STEREO;
}
- private MediaFormatProfile ResolveAudioMP4Format(int? bitrate)
+ private static MediaFormatProfile ResolveAudioMP4Format(int? bitrate)
{
if (bitrate.HasValue && bitrate.Value <= 320)
{
@@ -465,7 +456,7 @@ namespace MediaBrowser.Model.Dlna
return MediaFormatProfile.AAC_ISO;
}
- private MediaFormatProfile ResolveAudioADTSFormat(int? bitrate)
+ private static MediaFormatProfile ResolveAudioADTSFormat(int? bitrate)
{
if (bitrate.HasValue && bitrate.Value <= 320)
{
@@ -475,11 +466,13 @@ namespace MediaBrowser.Model.Dlna
return MediaFormatProfile.AAC_ADTS;
}
- public MediaFormatProfile? ResolveImageFormat(string container, int? width, int? height)
+ 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))
{
@@ -499,7 +492,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- private MediaFormatProfile ResolveImageJPGFormat(int? width, int? height)
+ private static MediaFormatProfile ResolveImageJPGFormat(int? width, int? height)
{
if (width.HasValue && height.HasValue)
{
@@ -524,7 +517,7 @@ namespace MediaBrowser.Model.Dlna
return MediaFormatProfile.JPEG_SM;
}
- private MediaFormatProfile ResolveImagePNGFormat(int? width, int? height)
+ private static MediaFormatProfile ResolveImagePNGFormat(int? width, int? height)
{
if (width.HasValue && height.HasValue)
{
diff --git a/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs
index 30c44fbe0..f8f76c69d 100644
--- a/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs
+++ b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs
@@ -4,14 +4,14 @@ namespace MediaBrowser.Model.Dlna
{
public class ResolutionConfiguration
{
- public int MaxWidth { get; set; }
-
- public int MaxBitrate { get; set; }
-
public ResolutionConfiguration(int maxWidth, int maxBitrate)
{
MaxWidth = maxWidth;
MaxBitrate = maxBitrate;
}
+
+ public int MaxWidth { get; set; }
+
+ public int MaxBitrate { get; set; }
}
}
diff --git a/MediaBrowser.Model/Dlna/ResponseProfile.cs b/MediaBrowser.Model/Dlna/ResponseProfile.cs
index 48f53f06c..bf9661f7f 100644
--- a/MediaBrowser.Model/Dlna/ResponseProfile.cs
+++ b/MediaBrowser.Model/Dlna/ResponseProfile.cs
@@ -8,6 +8,11 @@ namespace MediaBrowser.Model.Dlna
{
public class ResponseProfile
{
+ public ResponseProfile()
+ {
+ Conditions = Array.Empty<ProfileCondition>();
+ }
+
[XmlAttribute("container")]
public string Container { get; set; }
@@ -28,11 +33,6 @@ namespace MediaBrowser.Model.Dlna
public ProfileCondition[] Conditions { get; set; }
- public ResponseProfile()
- {
- Conditions = Array.Empty<ProfileCondition>();
- }
-
public string[] GetContainers()
{
return ContainerProfile.SplitValue(Container);
diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs
index 94f5bd3db..b1fc48c08 100644
--- a/MediaBrowser.Model/Dlna/SearchCriteria.cs
+++ b/MediaBrowser.Model/Dlna/SearchCriteria.cs
@@ -7,31 +7,6 @@ namespace MediaBrowser.Model.Dlna
{
public class SearchCriteria
{
- public SearchType SearchType { get; set; }
-
- /// <summary>
- /// Splits the specified string.
- /// </summary>
- /// <param name="str">The string.</param>
- /// <param name="term">The term.</param>
- /// <param name="limit">The limit.</param>
- /// <returns>System.String[].</returns>
- private static string[] RegexSplit(string str, string term, int limit)
- {
- return new Regex(term).Split(str, limit);
- }
-
- /// <summary>
- /// Splits the specified string.
- /// </summary>
- /// <param name="str">The string.</param>
- /// <param name="term">The term.</param>
- /// <returns>System.String[].</returns>
- private static string[] RegexSplit(string str, string term)
- {
- return Regex.Split(str, term, RegexOptions.IgnoreCase);
- }
-
public SearchCriteria(string search)
{
if (search.Length == 0)
@@ -48,8 +23,8 @@ namespace MediaBrowser.Model.Dlna
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("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))
{
@@ -71,5 +46,30 @@ namespace MediaBrowser.Model.Dlna
}
}
}
+
+ public SearchType SearchType { get; set; }
+
+ /// <summary>
+ /// Splits the specified string.
+ /// </summary>
+ /// <param name="str">The string.</param>
+ /// <param name="term">The term.</param>
+ /// <param name="limit">The limit.</param>
+ /// <returns>System.String[].</returns>
+ private static string[] RegexSplit(string str, string term, int limit)
+ {
+ return new Regex(term).Split(str, limit);
+ }
+
+ /// <summary>
+ /// Splits the specified string.
+ /// </summary>
+ /// <param name="str">The string.</param>
+ /// <param name="term">The term.</param>
+ /// <returns>System.String[].</returns>
+ private static string[] RegexSplit(string str, string term)
+ {
+ return Regex.Split(str, term, RegexOptions.IgnoreCase);
+ }
}
}
diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs
index 53e4540cb..7769d0bd3 100644
--- a/MediaBrowser.Model/Dlna/SortCriteria.cs
+++ b/MediaBrowser.Model/Dlna/SortCriteria.cs
@@ -6,10 +6,10 @@ namespace MediaBrowser.Model.Dlna
{
public class SortCriteria
{
- public SortOrder SortOrder => SortOrder.Ascending;
-
public SortCriteria(string value)
{
}
+
+ public SortOrder SortOrder => SortOrder.Ascending;
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 431cf0baf..f4c69fe8f 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -227,7 +227,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, string _, DeviceProfile profile, DlnaProfileType type)
+ public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type)
{
if (string.IsNullOrEmpty(inputContainer))
{
@@ -274,14 +274,14 @@ namespace MediaBrowser.Model.Dlna
if (options.ForceDirectPlay)
{
playlistItem.PlayMethod = PlayMethod.DirectPlay;
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
return playlistItem;
}
if (options.ForceDirectStream)
{
playlistItem.PlayMethod = PlayMethod.DirectStream;
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
return playlistItem;
}
@@ -297,7 +297,7 @@ namespace MediaBrowser.Model.Dlna
int? inputAudioSampleRate = audioStream?.SampleRate;
int? inputAudioBitDepth = audioStream?.BitDepth;
- if (directPlayMethods.Count() > 0)
+ if (directPlayMethods.Any())
{
string audioCodec = audioStream?.Codec;
@@ -349,7 +349,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.PlayMethod = PlayMethod.DirectStream;
}
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
return playlistItem;
}
@@ -514,6 +514,8 @@ namespace MediaBrowser.Model.Dlna
private static List<TranscodeReason> GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
{
+ var mediaType = videoStream == null ? DlnaProfileType.Audio : DlnaProfileType.Video;
+
var containerSupported = false;
var audioSupported = false;
var videoSupported = false;
@@ -521,7 +523,7 @@ namespace MediaBrowser.Model.Dlna
foreach (var profile in directPlayProfiles)
{
// Check container type
- if (profile.SupportsContainer(item.Container))
+ if (profile.Type == mediaType && profile.SupportsContainer(item.Container))
{
containerSupported = true;
@@ -674,7 +676,7 @@ namespace MediaBrowser.Model.Dlna
var videoStream = item.VideoStream;
- // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough
+ // TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough
var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, options, PlayMethod.DirectPlay);
var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, options, PlayMethod.DirectStream);
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1);
@@ -698,7 +700,7 @@ namespace MediaBrowser.Model.Dlna
if (directPlay != null)
{
playlistItem.PlayMethod = directPlay.Value;
- playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Video);
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video);
if (subtitleStream != null)
{
@@ -1017,14 +1019,15 @@ namespace MediaBrowser.Model.Dlna
}
DeviceProfile profile = options.Profile;
+ string container = mediaSource.Container;
// See if it can be direct played
DirectPlayProfile directPlay = null;
- foreach (var i in profile.DirectPlayProfiles)
+ foreach (var p in profile.DirectPlayProfiles)
{
- if (i.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(i, mediaSource, videoStream, audioStream))
+ if (p.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(p, container, videoStream, audioStream))
{
- directPlay = i;
+ directPlay = p;
break;
}
}
@@ -1032,23 +1035,23 @@ namespace MediaBrowser.Model.Dlna
if (directPlay == null)
{
_logger.LogInformation(
- "Profile: {0}, No video direct play profiles found for {1} with codec {2}",
- profile?.Name ?? "Unknown Profile",
- mediaSource?.Path ?? "Unknown path",
- videoStream?.Codec ?? "Unknown codec");
+ "Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}",
+ container,
+ videoStream?.Codec ?? "no video",
+ audioStream?.Codec ?? "no audio",
+ profile.Name ?? "unknown profile",
+ mediaSource.Path ?? "unknown path");
return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles));
}
- string container = mediaSource.Container;
-
var conditions = new List<ProfileCondition>();
- foreach (var i in profile.ContainerProfiles)
+ foreach (var p in profile.ContainerProfiles)
{
- if (i.Type == DlnaProfileType.Video
- && i.ContainsContainer(container))
+ if (p.Type == DlnaProfileType.Video
+ && p.ContainsContainer(container))
{
- foreach (var c in i.Conditions)
+ foreach (var c in p.Conditions)
{
conditions.Add(c);
}
@@ -1404,7 +1407,9 @@ namespace MediaBrowser.Model.Dlna
{
_logger.LogInformation(
"Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
- playMethod, itemBitrate, requestedMaxBitrate);
+ playMethod,
+ itemBitrate,
+ requestedMaxBitrate);
return false;
}
@@ -1733,7 +1738,7 @@ namespace MediaBrowser.Model.Dlna
if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny)
{
- item.SetOption(qualifier, "profile", string.Join(",", values));
+ item.SetOption(qualifier, "profile", string.Join(',', values));
}
}
@@ -1894,10 +1899,10 @@ namespace MediaBrowser.Model.Dlna
return true;
}
- private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
+ private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, string container, MediaStream videoStream, MediaStream audioStream)
{
// Check container type
- if (!profile.SupportsContainer(item.Container))
+ if (!profile.SupportsContainer(container))
{
return false;
}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 55b12ae81..252872847 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -27,45 +27,6 @@ namespace MediaBrowser.Model.Dlna
StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
- public void SetOption(string qualifier, string name, string value)
- {
- if (string.IsNullOrEmpty(qualifier))
- {
- SetOption(name, value);
- }
- else
- {
- SetOption(qualifier + "-" + name, value);
- }
- }
-
- public void SetOption(string name, string value)
- {
- StreamOptions[name] = value;
- }
-
- public string GetOption(string qualifier, string name)
- {
- var value = GetOption(qualifier + "-" + name);
-
- if (string.IsNullOrEmpty(value))
- {
- value = GetOption(name);
- }
-
- return value;
- }
-
- public string GetOption(string name)
- {
- if (StreamOptions.TryGetValue(name, out var value))
- {
- return value;
- }
-
- return null;
- }
-
public Guid ItemId { get; set; }
public PlayMethod PlayMethod { get; set; }
@@ -152,374 +113,18 @@ namespace MediaBrowser.Model.Dlna
PlayMethod == PlayMethod.DirectStream ||
PlayMethod == PlayMethod.DirectPlay;
- public string ToUrl(string baseUrl, string accessToken)
- {
- if (PlayMethod == PlayMethod.DirectPlay)
- {
- return MediaSource.Path;
- }
-
- if (string.IsNullOrEmpty(baseUrl))
- {
- throw new ArgumentNullException(nameof(baseUrl));
- }
-
- var list = new List<string>();
- foreach (NameValuePair pair in BuildParams(this, accessToken))
- {
- if (string.IsNullOrEmpty(pair.Value))
- {
- continue;
- }
-
- // Try to keep the url clean by omitting defaults
- if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- // Be careful, IsDirectStream==true by default (Static != false or not in query).
- // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
- if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(pair.Value, "true", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var encodedValue = pair.Value.Replace(" ", "%20");
-
- list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
- }
-
- string queryString = string.Join("&", list.ToArray());
-
- return GetUrl(baseUrl, queryString);
- }
-
- private string GetUrl(string baseUrl, string queryString)
- {
- if (string.IsNullOrEmpty(baseUrl))
- {
- throw new ArgumentNullException(nameof(baseUrl));
- }
-
- string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
-
- baseUrl = baseUrl.TrimEnd('/');
-
- if (MediaType == DlnaProfileType.Audio)
- {
- if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
- {
- return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
- }
-
- return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
- }
-
- if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
- {
- return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
- }
-
- return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
- }
-
- private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
- {
- var list = new List<NameValuePair>();
-
- string audioCodecs = item.AudioCodecs.Length == 0 ?
- string.Empty :
- string.Join(",", item.AudioCodecs);
-
- string videoCodecs = item.VideoCodecs.Length == 0 ?
- string.Empty :
- string.Join(",", item.VideoCodecs);
-
- list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
- list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
- list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
- list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- list.Add(new NameValuePair("VideoCodec", videoCodecs));
- list.Add(new NameValuePair("AudioCodec", audioCodecs));
- list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-
- list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-
- long startPositionTicks = item.StartPositionTicks;
-
- var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase);
-
- if (isHls)
- {
- list.Add(new NameValuePair("StartTimeTicks", string.Empty));
- }
- else
- {
- list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
- }
-
- list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
- list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
-
- string liveStreamId = item.MediaSource?.LiveStreamId;
- list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
-
- list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
-
- if (!item.IsDirectStream)
- {
- if (item.RequireNonAnamorphic)
- {
- list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
-
- if (item.EnableSubtitlesInManifest)
- {
- list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- if (item.EnableMpegtsM2TsMode)
- {
- list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- if (item.EstimateContentLength)
- {
- list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
- {
- list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
- }
-
- if (item.CopyTimestamps)
- {
- list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- }
-
- list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
-
- string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
- string.Empty :
- string.Join(",", item.SubtitleCodecs);
-
- list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
-
- if (isHls)
- {
- list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
-
- if (item.SegmentLength.HasValue)
- {
- list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
- }
-
- if (item.MinSegments.HasValue)
- {
- list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
- }
-
- list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
- }
-
- foreach (var pair in item.StreamOptions)
- {
- if (string.IsNullOrEmpty(pair.Value))
- {
- continue;
- }
-
- // strip spaces to avoid having to encode h264 profile names
- list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", "")));
- }
-
- if (!item.IsDirectStream)
- {
- list.Add(new NameValuePair("TranscodeReasons", string.Join(",", item.TranscodeReasons.Distinct().Select(i => i.ToString()))));
- }
-
- return list;
- }
-
- public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
- {
- return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
- }
-
- public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
- {
- var list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken);
- var newList = new List<SubtitleStreamInfo>();
-
- // First add the selected track
- foreach (SubtitleStreamInfo stream in list)
- {
- if (stream.DeliveryMethod == SubtitleDeliveryMethod.External)
- {
- newList.Add(stream);
- }
- }
-
- return newList;
- }
-
- public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
- {
- return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
- }
-
- public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
- {
- var list = new List<SubtitleStreamInfo>();
-
- // HLS will preserve timestamps so we can just grab the full subtitle stream
- long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)
- ? 0
- : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
-
- // First add the selected track
- if (SubtitleStreamIndex.HasValue)
- {
- foreach (var stream in MediaSource.MediaStreams)
- {
- if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
- {
- AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
- }
- }
- }
-
- if (!includeSelectedTrackOnly)
- {
- foreach (var stream in MediaSource.MediaStreams)
- {
- if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
- {
- AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
- }
- }
- }
-
- return list;
- }
-
- private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks)
- {
- if (enableAllProfiles)
- {
- foreach (var profile in DeviceProfile.SubtitleProfiles)
- {
- var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
-
- list.Add(info);
- }
- }
- else
- {
- var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
-
- list.Add(info);
- }
- }
-
- private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
- {
- var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
- var info = new SubtitleStreamInfo
- {
- IsForced = stream.IsForced,
- Language = stream.Language,
- Name = stream.Language ?? "Unknown",
- Format = subtitleProfile.Format,
- Index = stream.Index,
- DeliveryMethod = subtitleProfile.Method,
- DisplayTitle = stream.DisplayTitle
- };
-
- if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
- {
- if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
- {
- info.Url = string.Format(CultureInfo.InvariantCulture, "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
- baseUrl,
- ItemId,
- MediaSourceId,
- stream.Index.ToString(CultureInfo.InvariantCulture),
- startPositionTicks.ToString(CultureInfo.InvariantCulture),
- subtitleProfile.Format);
-
- if (!string.IsNullOrEmpty(accessToken))
- {
- info.Url += "?api_key=" + accessToken;
- }
-
- info.IsExternalUrl = false;
- }
- else
- {
- info.Url = stream.Path;
- info.IsExternalUrl = true;
- }
- }
-
- return info;
- }
-
/// <summary>
- /// Returns the audio stream that will be used.
+ /// Gets the audio stream that will be used.
/// </summary>
- public MediaStream TargetAudioStream
- {
- get
- {
- if (MediaSource != null)
- {
- return MediaSource.GetDefaultAudioStream(AudioStreamIndex);
- }
-
- return null;
- }
- }
+ public MediaStream TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
/// <summary>
- /// Returns the video stream that will be used.
+ /// Gets the video stream that will be used.
/// </summary>
- public MediaStream TargetVideoStream
- {
- get
- {
- if (MediaSource != null)
- {
- return MediaSource.VideoStream;
- }
-
- return null;
- }
- }
+ public MediaStream TargetVideoStream => MediaSource?.VideoStream;
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the audio sample rate that will be in the output stream.
/// </summary>
public int? TargetAudioSampleRate
{
@@ -533,7 +138,7 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the audio sample rate that will be in the output stream.
/// </summary>
public int? TargetAudioBitDepth
{
@@ -556,7 +161,7 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the audio sample rate that will be in the output stream.
/// </summary>
public int? TargetVideoBitDepth
{
@@ -603,7 +208,7 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the audio sample rate that will be in the output stream.
/// </summary>
public float? TargetFramerate
{
@@ -617,7 +222,7 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the audio sample rate that will be in the output stream.
/// </summary>
public double? TargetVideoLevel
{
@@ -639,72 +244,8 @@ namespace MediaBrowser.Model.Dlna
}
}
- public int? GetTargetVideoBitDepth(string codec)
- {
- var value = GetOption(codec, "videobitdepth");
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
-
- if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
-
- return null;
- }
-
- public int? GetTargetAudioBitDepth(string codec)
- {
- var value = GetOption(codec, "audiobitdepth");
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
-
- if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
-
- return null;
- }
-
- public double? GetTargetVideoLevel(string codec)
- {
- var value = GetOption(codec, "level");
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
-
- if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
-
- return null;
- }
-
- public int? GetTargetRefFrames(string codec)
- {
- var value = GetOption(codec, "maxrefframes");
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
-
- if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
-
- return null;
- }
-
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the audio sample rate that will be in the output stream.
/// </summary>
public int? TargetPacketLength
{
@@ -718,7 +259,7 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the audio sample rate that will be in the output stream.
/// </summary>
public string TargetVideoProfile
{
@@ -756,7 +297,7 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Predicts the audio bitrate that will be in the output stream.
+ /// Gets the audio bitrate that will be in the output stream.
/// </summary>
public int? TargetAudioBitrate
{
@@ -770,7 +311,7 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Predicts the audio channels that will be in the output stream.
+ /// Gets the audio channels that will be in the output stream.
/// </summary>
public int? TargetAudioChannels
{
@@ -792,26 +333,8 @@ namespace MediaBrowser.Model.Dlna
}
}
- public int? GetTargetAudioChannels(string codec)
- {
- var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
-
- var value = GetOption(codec, "audiochannels");
- if (string.IsNullOrEmpty(value))
- {
- return defaultValue;
- }
-
- if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
- {
- return Math.Min(result, defaultValue ?? result);
- }
-
- return defaultValue;
- }
-
/// <summary>
- /// Predicts the audio codec that will be in the output stream.
+ /// Gets the audio codec that will be in the output stream.
/// </summary>
public string[] TargetAudioCodec
{
@@ -864,7 +387,7 @@ namespace MediaBrowser.Model.Dlna
}
/// <summary>
- /// Predicts the audio channels that will be in the output stream.
+ /// Gets the audio channels that will be in the output stream.
/// </summary>
public long? TargetSize
{
@@ -1035,6 +558,461 @@ namespace MediaBrowser.Model.Dlna
}
}
+ public void SetOption(string qualifier, string name, string value)
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ SetOption(name, value);
+ }
+ else
+ {
+ SetOption(qualifier + "-" + name, value);
+ }
+ }
+
+ public void SetOption(string name, string value)
+ {
+ StreamOptions[name] = value;
+ }
+
+ public string GetOption(string qualifier, string name)
+ {
+ var value = GetOption(qualifier + "-" + name);
+
+ if (string.IsNullOrEmpty(value))
+ {
+ value = GetOption(name);
+ }
+
+ return value;
+ }
+
+ public string GetOption(string name)
+ {
+ if (StreamOptions.TryGetValue(name, out var value))
+ {
+ return value;
+ }
+
+ return null;
+ }
+
+ public string ToUrl(string baseUrl, string accessToken)
+ {
+ if (PlayMethod == PlayMethod.DirectPlay)
+ {
+ return MediaSource.Path;
+ }
+
+ if (string.IsNullOrEmpty(baseUrl))
+ {
+ throw new ArgumentNullException(nameof(baseUrl));
+ }
+
+ var list = new List<string>();
+ foreach (NameValuePair pair in BuildParams(this, accessToken))
+ {
+ if (string.IsNullOrEmpty(pair.Value))
+ {
+ continue;
+ }
+
+ // Try to keep the url clean by omitting defaults
+ if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var encodedValue = pair.Value.Replace(" ", "%20");
+
+ list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
+ }
+
+ string queryString = string.Join("&", list.ToArray());
+
+ return GetUrl(baseUrl, queryString);
+ }
+
+ private string GetUrl(string baseUrl, string queryString)
+ {
+ if (string.IsNullOrEmpty(baseUrl))
+ {
+ throw new ArgumentNullException(nameof(baseUrl));
+ }
+
+ string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
+
+ baseUrl = baseUrl.TrimEnd('/');
+
+ if (MediaType == DlnaProfileType.Audio)
+ {
+ if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ }
+
+ return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ }
+
+ if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ }
+
+ return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ }
+
+ private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
+ {
+ var list = new List<NameValuePair>();
+
+ string audioCodecs = item.AudioCodecs.Length == 0 ?
+ string.Empty :
+ string.Join(",", item.AudioCodecs);
+
+ string videoCodecs = item.VideoCodecs.Length == 0 ?
+ string.Empty :
+ string.Join(",", item.VideoCodecs);
+
+ list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
+ list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
+ list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
+ list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ list.Add(new NameValuePair("VideoCodec", videoCodecs));
+ list.Add(new NameValuePair("AudioCodec", audioCodecs));
+ list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ long startPositionTicks = item.StartPositionTicks;
+
+ var isHls = string.Equals(item.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase);
+
+ if (isHls)
+ {
+ list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+ }
+ else
+ {
+ list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
+ list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
+
+ string liveStreamId = item.MediaSource?.LiveStreamId;
+ list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
+
+ list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
+
+ if (!item.IsDirectStream)
+ {
+ if (item.RequireNonAnamorphic)
+ {
+ list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ if (item.EnableSubtitlesInManifest)
+ {
+ list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ if (item.EnableMpegtsM2TsMode)
+ {
+ list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ if (item.EstimateContentLength)
+ {
+ list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+ {
+ list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
+ }
+
+ if (item.CopyTimestamps)
+ {
+ list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
+
+ string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
+ string.Empty :
+ string.Join(",", item.SubtitleCodecs);
+
+ list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
+
+ if (isHls)
+ {
+ list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
+
+ if (item.SegmentLength.HasValue)
+ {
+ list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ if (item.MinSegments.HasValue)
+ {
+ list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ foreach (var pair in item.StreamOptions)
+ {
+ if (string.IsNullOrEmpty(pair.Value))
+ {
+ continue;
+ }
+
+ // strip spaces to avoid having to encode h264 profile names
+ list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty)));
+ }
+
+ if (!item.IsDirectStream)
+ {
+ list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct())));
+ }
+
+ return list;
+ }
+
+ public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
+ {
+ return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+ }
+
+ public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+ {
+ var list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken);
+ var newList = new List<SubtitleStreamInfo>();
+
+ // First add the selected track
+ foreach (SubtitleStreamInfo stream in list)
+ {
+ if (stream.DeliveryMethod == SubtitleDeliveryMethod.External)
+ {
+ newList.Add(stream);
+ }
+ }
+
+ return newList;
+ }
+
+ public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
+ {
+ return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+ }
+
+ public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+ {
+ var list = new List<SubtitleStreamInfo>();
+
+ // HLS will preserve timestamps so we can just grab the full subtitle stream
+ long startPositionTicks = string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)
+ ? 0
+ : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
+
+ // First add the selected track
+ if (SubtitleStreamIndex.HasValue)
+ {
+ foreach (var stream in MediaSource.MediaStreams)
+ {
+ if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
+ {
+ AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
+ }
+ }
+ }
+
+ if (!includeSelectedTrackOnly)
+ {
+ foreach (var stream in MediaSource.MediaStreams)
+ {
+ if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
+ {
+ AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks)
+ {
+ if (enableAllProfiles)
+ {
+ foreach (var profile in DeviceProfile.SubtitleProfiles)
+ {
+ var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
+
+ list.Add(info);
+ }
+ }
+ else
+ {
+ var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
+
+ list.Add(info);
+ }
+ }
+
+ private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
+ {
+ var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
+ var info = new SubtitleStreamInfo
+ {
+ IsForced = stream.IsForced,
+ Language = stream.Language,
+ Name = stream.Language ?? "Unknown",
+ Format = subtitleProfile.Format,
+ Index = stream.Index,
+ DeliveryMethod = subtitleProfile.Method,
+ DisplayTitle = stream.DisplayTitle
+ };
+
+ if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
+ {
+ if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
+ {
+ info.Url = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
+ baseUrl,
+ ItemId,
+ MediaSourceId,
+ stream.Index.ToString(CultureInfo.InvariantCulture),
+ startPositionTicks.ToString(CultureInfo.InvariantCulture),
+ subtitleProfile.Format);
+
+ if (!string.IsNullOrEmpty(accessToken))
+ {
+ info.Url += "?api_key=" + accessToken;
+ }
+
+ info.IsExternalUrl = false;
+ }
+ else
+ {
+ info.Url = stream.Path;
+ info.IsExternalUrl = true;
+ }
+ }
+
+ return info;
+ }
+
+ public int? GetTargetVideoBitDepth(string codec)
+ {
+ var value = GetOption(codec, "videobitdepth");
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ public int? GetTargetAudioBitDepth(string codec)
+ {
+ var value = GetOption(codec, "audiobitdepth");
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ public double? GetTargetVideoLevel(string codec)
+ {
+ var value = GetOption(codec, "level");
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ public int? GetTargetRefFrames(string codec)
+ {
+ var value = GetOption(codec, "maxrefframes");
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ public int? GetTargetAudioChannels(string codec)
+ {
+ var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
+
+ var value = GetOption(codec, "audiochannels");
+ if (string.IsNullOrEmpty(value))
+ {
+ return defaultValue;
+ }
+
+ if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+ {
+ return Math.Min(result, defaultValue ?? result);
+ }
+
+ return defaultValue;
+ }
+
private int? GetMediaStreamCount(MediaStreamType type, int limit)
{
var count = MediaSource.GetStreamCount(type);
diff --git a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs
index e7fe8d6af..9b39f9e11 100644
--- a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs
+++ b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs
@@ -2,25 +2,28 @@
namespace MediaBrowser.Model.Dlna
{
+ /// <summary>
+ /// Delivery method to use during playback of a specific subtitle format.
+ /// </summary>
public enum SubtitleDeliveryMethod
{
/// <summary>
- /// The encode.
+ /// Burn the subtitles in the video track.
/// </summary>
Encode = 0,
/// <summary>
- /// The embed.
+ /// Embed the subtitles in the file or stream.
/// </summary>
Embed = 1,
/// <summary>
- /// The external.
+ /// Serve the subtitles as an external file.
/// </summary>
External = 2,
/// <summary>
- /// The HLS.
+ /// Serve the subtitles as a separate HLS stream.
/// </summary>
Hls = 3
}
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index f05e31047..214578a85 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -1,6 +1,7 @@
-#nullable disable
#pragma warning disable CS1591
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
namespace MediaBrowser.Model.Dlna
@@ -8,47 +9,56 @@ namespace MediaBrowser.Model.Dlna
public class TranscodingProfile
{
[XmlAttribute("container")]
- public string Container { get; set; }
+ public string Container { get; set; } = string.Empty;
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
[XmlAttribute("videoCodec")]
- public string VideoCodec { get; set; }
+ public string VideoCodec { get; set; } = string.Empty;
[XmlAttribute("audioCodec")]
- public string AudioCodec { get; set; }
+ public string AudioCodec { get; set; } = string.Empty;
[XmlAttribute("protocol")]
- public string Protocol { get; set; }
+ public string Protocol { get; set; } = string.Empty;
+ [DefaultValue(false)]
[XmlAttribute("estimateContentLength")]
public bool EstimateContentLength { get; set; }
+ [DefaultValue(false)]
[XmlAttribute("enableMpegtsM2TsMode")]
public bool EnableMpegtsM2TsMode { get; set; }
+ [DefaultValue(TranscodeSeekInfo.Auto)]
[XmlAttribute("transcodeSeekInfo")]
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+ [DefaultValue(false)]
[XmlAttribute("copyTimestamps")]
public bool CopyTimestamps { get; set; }
+ [DefaultValue(EncodingContext.Streaming)]
[XmlAttribute("context")]
public EncodingContext Context { get; set; }
+ [DefaultValue(false)]
[XmlAttribute("enableSubtitlesInManifest")]
public bool EnableSubtitlesInManifest { get; set; }
[XmlAttribute("maxAudioChannels")]
- public string MaxAudioChannels { get; set; }
+ public string? MaxAudioChannels { get; set; }
+ [DefaultValue(0)]
[XmlAttribute("minSegments")]
public int MinSegments { get; set; }
+ [DefaultValue(0)]
[XmlAttribute("segmentLength")]
public int SegmentLength { get; set; }
+ [DefaultValue(false)]
[XmlAttribute("breakOnNonKeyFrames")]
public bool BreakOnNonKeyFrames { get; set; }
diff --git a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs
index d71013f01..987a3a908 100644
--- a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs
+++ b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs
@@ -16,5 +16,7 @@ namespace MediaBrowser.Model.Dlna
public IPAddress LocalIpAddress { get; set; }
public int LocalPort { get; set; }
+
+ public IPAddress RemoteIpAddress { get; set; }
}
}
diff --git a/MediaBrowser.Model/Drawing/DrawingUtils.cs b/MediaBrowser.Model/Drawing/DrawingUtils.cs
index 1512c5233..556792768 100644
--- a/MediaBrowser.Model/Drawing/DrawingUtils.cs
+++ b/MediaBrowser.Model/Drawing/DrawingUtils.cs
@@ -58,6 +58,52 @@ namespace MediaBrowser.Model.Drawing
}
/// <summary>
+ /// Scale down to fill box.
+ /// Returns original size if both width and height are null or zero.
+ /// </summary>
+ /// <param name="size">The original size object.</param>
+ /// <param name="fillWidth">A new fixed width, if desired.</param>
+ /// <param name="fillHeight">A new fixed height, if desired.</param>
+ /// <returns>A new size object or size.</returns>
+ public static ImageDimensions ResizeFill(
+ ImageDimensions size,
+ int? fillWidth,
+ int? fillHeight)
+ {
+ // Return original size if input is invalid.
+ if ((fillWidth == null || fillWidth == 0)
+ && (fillHeight == null || fillHeight == 0))
+ {
+ return size;
+ }
+
+ if (fillWidth == null || fillWidth == 0)
+ {
+ fillWidth = 1;
+ }
+
+ if (fillHeight == null || fillHeight == 0)
+ {
+ fillHeight = 1;
+ }
+
+ double widthRatio = size.Width / (double)fillWidth;
+ double heightRatio = size.Height / (double)fillHeight;
+ double scaleRatio = Math.Min(widthRatio, heightRatio);
+
+ // Clamp to current size.
+ if (scaleRatio < 1)
+ {
+ return size;
+ }
+
+ int newWidth = Convert.ToInt32(Math.Ceiling(size.Width / scaleRatio));
+ int newHeight = Convert.ToInt32(Math.Ceiling(size.Height / scaleRatio));
+
+ return new ImageDimensions(newWidth, newHeight);
+ }
+
+ /// <summary>
/// Gets the new width.
/// </summary>
/// <param name="currentHeight">Height of the current.</param>
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 2f9f9d3cd..a784025e3 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -294,13 +294,13 @@ namespace MediaBrowser.Model.Dto
public NameGuidPair[] GenreItems { get; set; }
/// <summary>
- /// If the item does not have a logo, this will hold the Id of the Parent that has one.
+ /// Gets or sets wether the item has a logo, this will hold the Id of the Parent that has one.
/// </summary>
/// <value>The parent logo item id.</value>
public string ParentLogoItemId { get; set; }
/// <summary>
- /// If the item does not have any backdrops, this will hold the Id of the Parent that has one.
+ /// Gets or sets wether the item has any backdrops, this will hold the Id of the Parent that has one.
/// </summary>
/// <value>The parent backdrop item id.</value>
public string ParentBackdropItemId { get; set; }
@@ -318,7 +318,7 @@ namespace MediaBrowser.Model.Dto
public int? LocalTrailerCount { get; set; }
/// <summary>
- /// User data for this item based on the user it's being requested for.
+ /// Gets or sets the user data for this item based on the user it's being requested for.
/// </summary>
/// <value>The user data.</value>
public UserItemDataDto UserData { get; set; }
@@ -506,7 +506,7 @@ namespace MediaBrowser.Model.Dto
public string ParentLogoImageTag { get; set; }
/// <summary>
- /// If the item does not have a art, this will hold the Id of the Parent that has one.
+ /// Gets or sets wether the item has fan art, this will hold the Id of the Parent that has one.
/// </summary>
/// <value>The parent art item id.</value>
public string ParentArtItemId { get; set; }
@@ -695,7 +695,7 @@ namespace MediaBrowser.Model.Dto
public string ChannelPrimaryImageTag { get; set; }
/// <summary>
- /// The start date of the recording, in UTC.
+ /// Gets or sets the start date of the recording, in UTC.
/// </summary>
public DateTime? StartDate { get; set; }
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index be682be23..ec3b37efa 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -12,6 +12,18 @@ namespace MediaBrowser.Model.Dto
{
public class MediaSourceInfo
{
+ public MediaSourceInfo()
+ {
+ Formats = Array.Empty<string>();
+ MediaStreams = new List<MediaStream>();
+ MediaAttachments = Array.Empty<MediaAttachment>();
+ RequiredHttpHeaders = new Dictionary<string, string>();
+ SupportsTranscoding = true;
+ SupportsDirectStream = true;
+ SupportsDirectPlay = true;
+ SupportsProbing = true;
+ }
+
public MediaProtocol Protocol { get; set; }
public string Id { get; set; }
@@ -31,6 +43,7 @@ namespace MediaBrowser.Model.Dto
public string Name { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether the media is remote.
/// Differentiate internet url vs local network.
/// </summary>
public bool IsRemote { get; set; }
@@ -95,16 +108,28 @@ namespace MediaBrowser.Model.Dto
public int? AnalyzeDurationMs { get; set; }
- public MediaSourceInfo()
+ [JsonIgnore]
+ public TranscodeReason[] TranscodeReasons { get; set; }
+
+ public int? DefaultAudioStreamIndex { get; set; }
+
+ public int? DefaultSubtitleStreamIndex { get; set; }
+
+ [JsonIgnore]
+ public MediaStream VideoStream
{
- Formats = Array.Empty<string>();
- MediaStreams = new List<MediaStream>();
- MediaAttachments = Array.Empty<MediaAttachment>();
- RequiredHttpHeaders = new Dictionary<string, string>();
- SupportsTranscoding = true;
- SupportsDirectStream = true;
- SupportsDirectPlay = true;
- SupportsProbing = true;
+ get
+ {
+ foreach (var i in MediaStreams)
+ {
+ if (i.Type == MediaStreamType.Video)
+ {
+ return i;
+ }
+ }
+
+ return null;
+ }
}
public void InferTotalBitrate(bool force = false)
@@ -134,13 +159,6 @@ namespace MediaBrowser.Model.Dto
}
}
- [JsonIgnore]
- public TranscodeReason[] TranscodeReasons { get; set; }
-
- public int? DefaultAudioStreamIndex { get; set; }
-
- public int? DefaultSubtitleStreamIndex { get; set; }
-
public MediaStream GetDefaultAudioStream(int? defaultIndex)
{
if (defaultIndex.HasValue)
@@ -175,23 +193,6 @@ namespace MediaBrowser.Model.Dto
return null;
}
- [JsonIgnore]
- public MediaStream VideoStream
- {
- get
- {
- foreach (var i in MediaStreams)
- {
- if (i.Type == MediaStreamType.Video)
- {
- return i;
- }
- }
-
- return null;
- }
- }
-
public MediaStream GetMediaStream(MediaStreamType type, int index)
{
foreach (var i in MediaStreams)
diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
index e4f38d6af..e0e889f7d 100644
--- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
+++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
@@ -10,6 +10,15 @@ namespace MediaBrowser.Model.Dto
{
public class MetadataEditorInfo
{
+ public MetadataEditorInfo()
+ {
+ ParentalRatingOptions = Array.Empty<ParentalRating>();
+ Countries = Array.Empty<CountryInfo>();
+ Cultures = Array.Empty<CultureDto>();
+ ExternalIdInfos = Array.Empty<ExternalIdInfo>();
+ ContentTypeOptions = Array.Empty<NameValuePair>();
+ }
+
public ParentalRating[] ParentalRatingOptions { get; set; }
public CountryInfo[] Countries { get; set; }
@@ -21,14 +30,5 @@ namespace MediaBrowser.Model.Dto
public string ContentType { get; set; }
public NameValuePair[] ContentTypeOptions { get; set; }
-
- public MetadataEditorInfo()
- {
- ParentalRatingOptions = Array.Empty<ParentalRating>();
- Countries = Array.Empty<CountryInfo>();
- Cultures = Array.Empty<CultureDto>();
- ExternalIdInfos = Array.Empty<ExternalIdInfo>();
- ContentTypeOptions = Array.Empty<NameValuePair>();
- }
}
}
diff --git a/MediaBrowser.Model/Dto/NameGuidPair.cs b/MediaBrowser.Model/Dto/NameGuidPair.cs
new file mode 100644
index 000000000..71166df97
--- /dev/null
+++ b/MediaBrowser.Model/Dto/NameGuidPair.cs
@@ -0,0 +1,14 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ public class NameGuidPair
+ {
+ public string Name { get; set; }
+
+ public Guid Id { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/NameIdPair.cs b/MediaBrowser.Model/Dto/NameIdPair.cs
index 45c2fb35d..31516947f 100644
--- a/MediaBrowser.Model/Dto/NameIdPair.cs
+++ b/MediaBrowser.Model/Dto/NameIdPair.cs
@@ -1,8 +1,6 @@
#nullable disable
#pragma warning disable CS1591
-using System;
-
namespace MediaBrowser.Model.Dto
{
public class NameIdPair
@@ -19,11 +17,4 @@ namespace MediaBrowser.Model.Dto
/// <value>The identifier.</value>
public string Id { get; set; }
}
-
- public class NameGuidPair
- {
- public string Name { get; set; }
-
- public Guid Id { get; set; }
- }
}
diff --git a/MediaBrowser.Model/Dto/UserDto.cs b/MediaBrowser.Model/Dto/UserDto.cs
index 40222c9dc..256d7b10f 100644
--- a/MediaBrowser.Model/Dto/UserDto.cs
+++ b/MediaBrowser.Model/Dto/UserDto.cs
@@ -11,6 +11,15 @@ namespace MediaBrowser.Model.Dto
public class UserDto : IItemDto, IHasServerId
{
/// <summary>
+ /// Initializes a new instance of the <see cref="UserDto"/> class.
+ /// </summary>
+ public UserDto()
+ {
+ Configuration = new UserConfiguration();
+ Policy = new UserPolicy();
+ }
+
+ /// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
@@ -94,15 +103,6 @@ namespace MediaBrowser.Model.Dto
/// <value>The primary image aspect ratio.</value>
public double? PrimaryImageAspectRatio { get; set; }
- /// <summary>
- /// Initializes a new instance of the <see cref="UserDto"/> class.
- /// </summary>
- public UserDto()
- {
- Configuration = new UserConfiguration();
- Policy = new UserPolicy();
- }
-
/// <inheritdoc />
public override string ToString()
{
diff --git a/MediaBrowser.Model/Entities/CollectionType.cs b/MediaBrowser.Model/Entities/CollectionType.cs
index 354038712..60b69d4b0 100644
--- a/MediaBrowser.Model/Entities/CollectionType.cs
+++ b/MediaBrowser.Model/Entities/CollectionType.cs
@@ -24,36 +24,4 @@ namespace MediaBrowser.Model.Entities
public const string Playlists = "playlists";
public const string Folders = "folders";
}
-
- public static class SpecialFolder
- {
- public const string TvShowSeries = "TvShowSeries";
- public const string TvGenres = "TvGenres";
- public const string TvGenre = "TvGenre";
- public const string TvLatest = "TvLatest";
- public const string TvNextUp = "TvNextUp";
- public const string TvResume = "TvResume";
- public const string TvFavoriteSeries = "TvFavoriteSeries";
- public const string TvFavoriteEpisodes = "TvFavoriteEpisodes";
-
- public const string MovieLatest = "MovieLatest";
- public const string MovieResume = "MovieResume";
- public const string MovieMovies = "MovieMovies";
- public const string MovieCollections = "MovieCollections";
- public const string MovieFavorites = "MovieFavorites";
- public const string MovieGenres = "MovieGenres";
- public const string MovieGenre = "MovieGenre";
-
- public const string MusicArtists = "MusicArtists";
- public const string MusicAlbumArtists = "MusicAlbumArtists";
- public const string MusicAlbums = "MusicAlbums";
- public const string MusicGenres = "MusicGenres";
- public const string MusicLatest = "MusicLatest";
- public const string MusicPlaylists = "MusicPlaylists";
- public const string MusicSongs = "MusicSongs";
- public const string MusicFavorites = "MusicFavorites";
- public const string MusicFavoriteArtists = "MusicFavoriteArtists";
- public const string MusicFavoriteAlbums = "MusicFavoriteAlbums";
- public const string MusicFavoriteSongs = "MusicFavoriteSongs";
- }
}
diff --git a/MediaBrowser.Model/Entities/CollectionTypeOptions.cs b/MediaBrowser.Model/Entities/CollectionTypeOptions.cs
new file mode 100644
index 000000000..e1894d84a
--- /dev/null
+++ b/MediaBrowser.Model/Entities/CollectionTypeOptions.cs
@@ -0,0 +1,16 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Entities
+{
+ public enum CollectionTypeOptions
+ {
+ Movies = 0,
+ TvShows = 1,
+ Music = 2,
+ MusicVideos = 3,
+ HomeVideos = 4,
+ BoxSets = 5,
+ Books = 6,
+ Mixed = 7
+ }
+}
diff --git a/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs b/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs
new file mode 100644
index 000000000..7c627f0e3
--- /dev/null
+++ b/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs
@@ -0,0 +1,29 @@
+#nullable disable
+// THIS IS A HACK
+// TODO: @bond Move to separate project
+
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Converts an object to a lowercase string.
+ /// </summary>
+ /// <typeparam name="T">The object type.</typeparam>
+ public class JsonLowerCaseConverter<T> : JsonConverter<T>
+ {
+ /// <inheritdoc />
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return JsonSerializer.Deserialize<T>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value?.ToString().ToLowerInvariant());
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index ca0b93c30..c67f30d04 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -84,7 +84,7 @@ namespace MediaBrowser.Model.Entities
public string Title { get; set; }
/// <summary>
- /// Gets or sets the video range.
+ /// Gets the video range.
/// </summary>
/// <value>The video range.</value>
public string VideoRange
@@ -104,15 +104,28 @@ namespace MediaBrowser.Model.Entities
return "HDR";
}
+ // For some Dolby Vision files, no color transfer is provided, so check the codec
+
+ var codecTag = CodecTag;
+
+ if (string.Equals(codecTag, "dva1", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codecTag, "dvav", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
+ {
+ return "HDR";
+ }
+
return "SDR";
}
}
- public string localizedUndefined { get; set; }
+ public string LocalizedUndefined { get; set; }
- public string localizedDefault { get; set; }
+ public string LocalizedDefault { get; set; }
- public string localizedForced { get; set; }
+ public string LocalizedForced { get; set; }
public string DisplayTitle
{
@@ -154,7 +167,7 @@ namespace MediaBrowser.Model.Entities
if (IsDefault)
{
- attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+ attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault);
}
if (!string.IsNullOrEmpty(Title))
@@ -163,7 +176,7 @@ namespace MediaBrowser.Model.Entities
foreach (var tag in attributes)
{
// Keep Tags that are not already in Title.
- if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ if (!Title.Contains(tag, StringComparison.OrdinalIgnoreCase))
{
result.Append(" - ").Append(tag);
}
@@ -202,7 +215,7 @@ namespace MediaBrowser.Model.Entities
foreach (var tag in attributes)
{
// Keep Tags that are not already in Title.
- if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ if (!Title.Contains(tag, StringComparison.OrdinalIgnoreCase))
{
result.Append(" - ").Append(tag);
}
@@ -211,7 +224,7 @@ namespace MediaBrowser.Model.Entities
return result.ToString();
}
- return string.Join(" ", attributes);
+ return string.Join(' ', attributes);
}
case MediaStreamType.Subtitle:
@@ -229,17 +242,17 @@ namespace MediaBrowser.Model.Entities
}
else
{
- attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
+ attributes.Add(string.IsNullOrEmpty(LocalizedUndefined) ? "Und" : LocalizedUndefined);
}
if (IsDefault)
{
- attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+ attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault);
}
if (IsForced)
{
- attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
+ attributes.Add(string.IsNullOrEmpty(LocalizedForced) ? "Forced" : LocalizedForced);
}
if (!string.IsNullOrEmpty(Title))
@@ -266,67 +279,6 @@ namespace MediaBrowser.Model.Entities
}
}
- private string GetResolutionText()
- {
- var i = this;
-
- if (i.Width.HasValue && i.Height.HasValue)
- {
- var width = i.Width.Value;
- var height = i.Height.Value;
-
- if (width >= 3800 || height >= 2000)
- {
- return "4K";
- }
-
- if (width >= 2500)
- {
- if (i.IsInterlaced)
- {
- return "1440i";
- }
-
- return "1440p";
- }
-
- if (width >= 1900 || height >= 1000)
- {
- if (i.IsInterlaced)
- {
- return "1080i";
- }
-
- return "1080p";
- }
-
- if (width >= 1260 || height >= 700)
- {
- if (i.IsInterlaced)
- {
- return "720i";
- }
-
- return "720p";
- }
-
- if (width >= 700 || height >= 440)
- {
-
- if (i.IsInterlaced)
- {
- return "480i";
- }
-
- return "480p";
- }
-
- return "SD";
- }
-
- return null;
- }
-
public string NalLengthSize { get; set; }
/// <summary>
@@ -487,15 +439,105 @@ namespace MediaBrowser.Model.Entities
}
}
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports external stream].
+ /// </summary>
+ /// <value><c>true</c> if [supports external stream]; otherwise, <c>false</c>.</value>
+ public bool SupportsExternalStream { get; set; }
+
+ /// <summary>
+ /// Gets or sets the filename.
+ /// </summary>
+ /// <value>The filename.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the pixel format.
+ /// </summary>
+ /// <value>The pixel format.</value>
+ public string PixelFormat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level.
+ /// </summary>
+ /// <value>The level.</value>
+ public double? Level { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether this instance is anamorphic.
+ /// </summary>
+ /// <value><c>true</c> if this instance is anamorphic; otherwise, <c>false</c>.</value>
+ public bool? IsAnamorphic { get; set; }
+
+ private string GetResolutionText()
+ {
+ var i = this;
+
+ if (i.Width.HasValue && i.Height.HasValue)
+ {
+ var width = i.Width.Value;
+ var height = i.Height.Value;
+
+ if (width >= 3800 || height >= 2000)
+ {
+ return "4K";
+ }
+
+ if (width >= 2500)
+ {
+ if (i.IsInterlaced)
+ {
+ return "1440i";
+ }
+
+ return "1440p";
+ }
+
+ if (width >= 1900 || height >= 1000)
+ {
+ if (i.IsInterlaced)
+ {
+ return "1080i";
+ }
+
+ return "1080p";
+ }
+
+ if (width >= 1260 || height >= 700)
+ {
+ if (i.IsInterlaced)
+ {
+ return "720i";
+ }
+
+ return "720p";
+ }
+
+ if (width >= 700 || height >= 440)
+ {
+ if (i.IsInterlaced)
+ {
+ return "480i";
+ }
+
+ return "480p";
+ }
+
+ return "SD";
+ }
+
+ return null;
+ }
+
public static bool IsTextFormat(string format)
{
string codec = format ?? string.Empty;
// sub = external .sub file
- return codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) == -1 &&
- codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) == -1 &&
- codec.IndexOf("dvbsub", StringComparison.OrdinalIgnoreCase) == -1 &&
+ return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) &&
+ !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase) &&
+ !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase);
}
@@ -533,35 +575,5 @@ namespace MediaBrowser.Model.Entities
return true;
}
-
- /// <summary>
- /// Gets or sets a value indicating whether [supports external stream].
- /// </summary>
- /// <value><c>true</c> if [supports external stream]; otherwise, <c>false</c>.</value>
- public bool SupportsExternalStream { get; set; }
-
- /// <summary>
- /// Gets or sets the filename.
- /// </summary>
- /// <value>The filename.</value>
- public string Path { get; set; }
-
- /// <summary>
- /// Gets or sets the pixel format.
- /// </summary>
- /// <value>The pixel format.</value>
- public string PixelFormat { get; set; }
-
- /// <summary>
- /// Gets or sets the level.
- /// </summary>
- /// <value>The level.</value>
- public double? Level { get; set; }
-
- /// <summary>
- /// Gets a value indicating whether this instance is anamorphic.
- /// </summary>
- /// <value><c>true</c> if this instance is anamorphic; otherwise, <c>false</c>.</value>
- public bool? IsAnamorphic { get; set; }
}
}
diff --git a/MediaBrowser.Model/Entities/PackageReviewInfo.cs b/MediaBrowser.Model/Entities/PackageReviewInfo.cs
deleted file mode 100644
index 5b22b34ac..000000000
--- a/MediaBrowser.Model/Entities/PackageReviewInfo.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Entities
-{
- public class PackageReviewInfo
- {
- /// <summary>
- /// Gets or sets the package id (database key) for this review.
- /// </summary>
- public int id { get; set; }
-
- /// <summary>
- /// Gets or sets the rating value.
- /// </summary>
- public int rating { get; set; }
-
- /// <summary>
- /// Gets or sets whether or not this review recommends this item.
- /// </summary>
- public bool recommend { get; set; }
-
- /// <summary>
- /// Gets or sets a short description of the review.
- /// </summary>
- public string title { get; set; }
-
- /// <summary>
- /// Gets or sets the full review.
- /// </summary>
- public string review { get; set; }
-
- /// <summary>
- /// Gets or sets the time of review.
- /// </summary>
- public DateTime timestamp { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
index 98097477c..ce4b0ec92 100644
--- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
+++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
namespace MediaBrowser.Model.Entities
{
@@ -9,25 +10,30 @@ namespace MediaBrowser.Model.Entities
public static class ProviderIdsExtensions
{
/// <summary>
- /// Determines whether [has provider identifier] [the specified instance].
+ /// Checks if this instance has an id for the given provider.
/// </summary>
/// <param name="instance">The instance.</param>
- /// <param name="provider">The provider.</param>
- /// <returns><c>true</c> if [has provider identifier] [the specified instance]; otherwise, <c>false</c>.</returns>
- public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider)
+ /// <param name="name">The of the provider name.</param>
+ /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
+ public static bool HasProviderId(this IHasProviderIds instance, string name)
{
- return !string.IsNullOrEmpty(instance.GetProviderId(provider.ToString()));
+ if (instance == null)
+ {
+ throw new ArgumentNullException(nameof(instance));
+ }
+
+ return instance.TryGetProviderId(name, out _);
}
/// <summary>
- /// Gets a provider id.
+ /// Checks if this instance has an id for the given provider.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="provider">The provider.</param>
- /// <returns>System.String.</returns>
- public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider)
+ /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
+ public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider)
{
- return instance.GetProviderId(provider.ToString());
+ return instance.HasProviderId(provider.ToString());
}
/// <summary>
@@ -35,8 +41,9 @@ namespace MediaBrowser.Model.Entities
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="name">The name.</param>
- /// <returns>System.String.</returns>
- public static string? GetProviderId(this IHasProviderIds instance, string name)
+ /// <param name="id">The provider id.</param>
+ /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
+ public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id)
{
if (instance == null)
{
@@ -45,11 +52,54 @@ namespace MediaBrowser.Model.Entities
if (instance.ProviderIds == null)
{
- return null;
+ id = null;
+ return false;
+ }
+
+ var foundProviderId = instance.ProviderIds.TryGetValue(name, out id);
+ // This occurs when searching with Identify (and possibly in other places)
+ if (string.IsNullOrEmpty(id))
+ {
+ id = null;
+ foundProviderId = false;
}
- instance.ProviderIds.TryGetValue(name, out string? id);
- return string.IsNullOrEmpty(id) ? null : id;
+ return foundProviderId;
+ }
+
+ /// <summary>
+ /// Gets a provider id.
+ /// </summary>
+ /// <param name="instance">The instance.</param>
+ /// <param name="provider">The provider.</param>
+ /// <param name="id">The provider id.</param>
+ /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
+ public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id)
+ {
+ return instance.TryGetProviderId(provider.ToString(), out id);
+ }
+
+ /// <summary>
+ /// Gets a provider id.
+ /// </summary>
+ /// <param name="instance">The instance.</param>
+ /// <param name="name">The name.</param>
+ /// <returns>System.String.</returns>
+ public static string? GetProviderId(this IHasProviderIds instance, string name)
+ {
+ instance.TryGetProviderId(name, out string? id);
+ return id;
+ }
+
+ /// <summary>
+ /// Gets a provider id.
+ /// </summary>
+ /// <param name="instance">The instance.</param>
+ /// <param name="provider">The provider.</param>
+ /// <returns>System.String.</returns>
+ public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider)
+ {
+ return instance.GetProviderId(provider.ToString());
}
/// <summary>
@@ -68,21 +118,12 @@ namespace MediaBrowser.Model.Entities
// If it's null remove the key from the dictionary
if (string.IsNullOrEmpty(value))
{
- if (instance.ProviderIds != null)
- {
- if (instance.ProviderIds.ContainsKey(name))
- {
- instance.ProviderIds.Remove(name);
- }
- }
+ instance.ProviderIds?.Remove(name);
}
else
{
// Ensure it exists
- if (instance.ProviderIds == null)
- {
- instance.ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
+ instance.ProviderIds ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
instance.ProviderIds[name] = value;
}
diff --git a/MediaBrowser.Model/Entities/SpecialFolder.cs b/MediaBrowser.Model/Entities/SpecialFolder.cs
new file mode 100644
index 000000000..2250c5dff
--- /dev/null
+++ b/MediaBrowser.Model/Entities/SpecialFolder.cs
@@ -0,0 +1,36 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Entities
+{
+ public static class SpecialFolder
+ {
+ public const string TvShowSeries = "TvShowSeries";
+ public const string TvGenres = "TvGenres";
+ public const string TvGenre = "TvGenre";
+ public const string TvLatest = "TvLatest";
+ public const string TvNextUp = "TvNextUp";
+ public const string TvResume = "TvResume";
+ public const string TvFavoriteSeries = "TvFavoriteSeries";
+ public const string TvFavoriteEpisodes = "TvFavoriteEpisodes";
+
+ public const string MovieLatest = "MovieLatest";
+ public const string MovieResume = "MovieResume";
+ public const string MovieMovies = "MovieMovies";
+ public const string MovieCollections = "MovieCollections";
+ public const string MovieFavorites = "MovieFavorites";
+ public const string MovieGenres = "MovieGenres";
+ public const string MovieGenre = "MovieGenre";
+
+ public const string MusicArtists = "MusicArtists";
+ public const string MusicAlbumArtists = "MusicAlbumArtists";
+ public const string MusicAlbums = "MusicAlbums";
+ public const string MusicGenres = "MusicGenres";
+ public const string MusicLatest = "MusicLatest";
+ public const string MusicPlaylists = "MusicPlaylists";
+ public const string MusicSongs = "MusicSongs";
+ public const string MusicFavorites = "MusicFavorites";
+ public const string MusicFavoriteArtists = "MusicFavoriteArtists";
+ public const string MusicFavoriteAlbums = "MusicFavoriteAlbums";
+ public const string MusicFavoriteSongs = "MusicFavoriteSongs";
+ }
+}
diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
index f2bc6f25e..8fed392b9 100644
--- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
+++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using System.Text.Json.Serialization;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Model.Entities
@@ -12,6 +13,14 @@ namespace MediaBrowser.Model.Entities
public class VirtualFolderInfo
{
/// <summary>
+ /// Initializes a new instance of the <see cref="VirtualFolderInfo"/> class.
+ /// </summary>
+ public VirtualFolderInfo()
+ {
+ Locations = Array.Empty<string>();
+ }
+
+ /// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
@@ -27,19 +36,12 @@ namespace MediaBrowser.Model.Entities
/// Gets or sets the type of the collection.
/// </summary>
/// <value>The type of the collection.</value>
- public string CollectionType { get; set; }
+ [JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))]
+ public CollectionTypeOptions? CollectionType { get; set; }
public LibraryOptions LibraryOptions { get; set; }
/// <summary>
- /// Initializes a new instance of the <see cref="VirtualFolderInfo"/> class.
- /// </summary>
- public VirtualFolderInfo()
- {
- Locations = Array.Empty<string>();
- }
-
- /// <summary>
/// Gets or sets the item identifier.
/// </summary>
/// <value>The item identifier.</value>
diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs
index 2d9a6c4db..77cbef00f 100644
--- a/MediaBrowser.Model/Extensions/StringHelper.cs
+++ b/MediaBrowser.Model/Extensions/StringHelper.cs
@@ -17,7 +17,8 @@ namespace MediaBrowser.Model.Extensions
return str;
}
- if (char.IsUpper(str[0]))
+ // We check IsLower instead of IsUpper because both return false for non-letters
+ if (!char.IsLower(str[0]))
{
return str;
}
diff --git a/MediaBrowser.Model/Globalization/CultureDto.cs b/MediaBrowser.Model/Globalization/CultureDto.cs
index 6af4a872c..5246f87d9 100644
--- a/MediaBrowser.Model/Globalization/CultureDto.cs
+++ b/MediaBrowser.Model/Globalization/CultureDto.cs
@@ -10,6 +10,11 @@ namespace MediaBrowser.Model.Globalization
/// </summary>
public class CultureDto
{
+ public CultureDto()
+ {
+ ThreeLetterISOLanguageNames = Array.Empty<string>();
+ }
+
/// <summary>
/// Gets or sets the name.
/// </summary>
@@ -29,7 +34,7 @@ namespace MediaBrowser.Model.Globalization
public string TwoLetterISOLanguageName { get; set; }
/// <summary>
- /// Gets or sets the name of the three letter ISO language.
+ /// Gets the name of the three letter ISO language.
/// </summary>
/// <value>The name of the three letter ISO language.</value>
public string ThreeLetterISOLanguageName
@@ -47,10 +52,5 @@ namespace MediaBrowser.Model.Globalization
}
public string[] ThreeLetterISOLanguageNames { get; set; }
-
- public CultureDto()
- {
- ThreeLetterISOLanguageNames = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Model/IO/FileSystemMetadata.cs b/MediaBrowser.Model/IO/FileSystemMetadata.cs
index 118c78e80..fb74886bf 100644
--- a/MediaBrowser.Model/IO/FileSystemMetadata.cs
+++ b/MediaBrowser.Model/IO/FileSystemMetadata.cs
@@ -38,12 +38,6 @@ namespace MediaBrowser.Model.IO
public long Length { get; set; }
/// <summary>
- /// Gets or sets the name of the directory.
- /// </summary>
- /// <value>The name of the directory.</value>
- public string DirectoryName { get; set; }
-
- /// <summary>
/// Gets or sets the last write time UTC.
/// </summary>
/// <value>The last write time UTC.</value>
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index dc6549787..be4f1e16b 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -25,7 +24,7 @@ namespace MediaBrowser.Model.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
- string ResolveShortcut(string filename);
+ string? ResolveShortcut(string filename);
/// <summary>
/// Creates the shortcut.
@@ -118,13 +117,6 @@ namespace MediaBrowser.Model.IO
bool ContainsSubPath(string parentPath, string path);
/// <summary>
- /// Determines whether [is root path] [the specified path].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns><c>true</c> if [is root path] [the specified path]; otherwise, <c>false</c>.</returns>
- bool IsRootPath(string path);
-
- /// <summary>
/// Normalizes the path.
/// </summary>
/// <param name="path">The path.</param>
@@ -155,16 +147,19 @@ namespace MediaBrowser.Model.IO
/// Gets the directories.
/// </summary>
/// <param name="path">The path.</param>
- /// <param name="recursive">if set to <c>true</c> [recursive].</param>
- /// <returns>IEnumerable&lt;DirectoryInfo&gt;.</returns>
+ /// <param name="recursive">If set to <c>true</c> also searches in subdirectiories.</param>
+ /// <returns>All found directories.</returns>
IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false);
/// <summary>
/// Gets the files.
/// </summary>
+ /// <param name="path">The path in which to search.</param>
+ /// <param name="recursive">If set to <c>true</c> also searches in subdirectiories.</param>
+ /// <returns>All found files.</returns>
IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false);
- IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive);
+ IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive);
/// <summary>
/// Gets the file system entries.
@@ -190,7 +185,7 @@ namespace MediaBrowser.Model.IO
/// <returns>IEnumerable&lt;System.String&gt;.</returns>
IEnumerable<string> GetFilePaths(string path, bool recursive = false);
- IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive);
+ IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive);
/// <summary>
/// Gets the file system entry paths.
diff --git a/MediaBrowser.Model/IO/IShortcutHandler.cs b/MediaBrowser.Model/IO/IShortcutHandler.cs
index 14d5c4b62..2c364a962 100644
--- a/MediaBrowser.Model/IO/IShortcutHandler.cs
+++ b/MediaBrowser.Model/IO/IShortcutHandler.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.IO
/// </summary>
/// <param name="shortcutPath">The shortcut path.</param>
/// <returns>System.String.</returns>
- string Resolve(string shortcutPath);
+ string? Resolve(string shortcutPath);
/// <summary>
/// Creates the specified shortcut path.
diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs
index 0e09db16e..f900da556 100644
--- a/MediaBrowser.Model/IO/IStreamHelper.cs
+++ b/MediaBrowser.Model/IO/IStreamHelper.cs
@@ -9,7 +9,7 @@ namespace MediaBrowser.Model.IO
{
public interface IStreamHelper
{
- Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken);
+ Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken);
Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken);
diff --git a/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs
index 07e76d960..c6de4c1ab 100644
--- a/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs
+++ b/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs
@@ -9,7 +9,7 @@ namespace MediaBrowser.Model.LiveTv
public class BaseTimerInfoDto : IHasServerId
{
/// <summary>
- /// Id of the recording.
+ /// Gets or sets the Id of the recording.
/// </summary>
public string Id { get; set; }
@@ -28,7 +28,7 @@ namespace MediaBrowser.Model.LiveTv
public string ExternalId { get; set; }
/// <summary>
- /// ChannelId of the recording.
+ /// Gets or sets the channel id of the recording.
/// </summary>
public Guid ChannelId { get; set; }
@@ -39,7 +39,7 @@ namespace MediaBrowser.Model.LiveTv
public string ExternalChannelId { get; set; }
/// <summary>
- /// ChannelName of the recording.
+ /// Gets or sets the channel name of the recording.
/// </summary>
public string ChannelName { get; set; }
@@ -58,22 +58,22 @@ namespace MediaBrowser.Model.LiveTv
public string ExternalProgramId { get; set; }
/// <summary>
- /// Name of the recording.
+ /// Gets or sets the name of the recording.
/// </summary>
public string Name { get; set; }
/// <summary>
- /// Description of the recording.
+ /// Gets or sets the description of the recording.
/// </summary>
public string Overview { get; set; }
/// <summary>
- /// The start date of the recording, in UTC.
+ /// Gets or sets the start date of the recording, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the recording, in UTC.
+ /// Gets or sets the end date of the recording, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
@@ -108,7 +108,7 @@ namespace MediaBrowser.Model.LiveTv
public bool IsPrePaddingRequired { get; set; }
/// <summary>
- /// If the item does not have any backdrops, this will hold the Id of the Parent that has one.
+ /// Gets or sets the Id of the Parent that has a backdrop if the item does not have one.
/// </summary>
/// <value>The parent backdrop item id.</value>
public string ParentBackdropItemId { get; set; }
diff --git a/MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs b/MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs
new file mode 100644
index 000000000..082daeb51
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/ListingsProviderInfo.cs
@@ -0,0 +1,58 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class ListingsProviderInfo
+ {
+ public ListingsProviderInfo()
+ {
+ NewsCategories = new[] { "news", "journalism", "documentary", "current affairs" };
+ SportsCategories = new[] { "sports", "basketball", "baseball", "football" };
+ KidsCategories = new[] { "kids", "family", "children", "childrens", "disney" };
+ MovieCategories = new[] { "movie" };
+ EnabledTuners = Array.Empty<string>();
+ EnableAllTuners = true;
+ ChannelMappings = Array.Empty<NameValuePair>();
+ }
+
+ public string Id { get; set; }
+
+ public string Type { get; set; }
+
+ public string Username { get; set; }
+
+ public string Password { get; set; }
+
+ public string ListingsId { get; set; }
+
+ public string ZipCode { get; set; }
+
+ public string Country { get; set; }
+
+ public string Path { get; set; }
+
+ public string[] EnabledTuners { get; set; }
+
+ public bool EnableAllTuners { get; set; }
+
+ public string[] NewsCategories { get; set; }
+
+ public string[] SportsCategories { get; set; }
+
+ public string[] KidsCategories { get; set; }
+
+ public string[] MovieCategories { get; set; }
+
+ public NameValuePair[] ChannelMappings { get; set; }
+
+ public string MoviePrefix { get; set; }
+
+ public string PreferredLanguage { get; set; }
+
+ public string UserAgent { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
index bcba344cc..673d97a9e 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
@@ -11,6 +11,12 @@ namespace MediaBrowser.Model.LiveTv
/// </summary>
public class LiveTvChannelQuery
{
+ public LiveTvChannelQuery()
+ {
+ EnableUserData = true;
+ SortBy = Array.Empty<string>();
+ }
+
/// <summary>
/// Gets or sets the type of the channel.
/// </summary>
@@ -48,13 +54,13 @@ namespace MediaBrowser.Model.LiveTv
public Guid UserId { get; set; }
/// <summary>
- /// Skips over a given number of items within the results. Use for paging.
+ /// Gets or sets the start index. Used for paging.
/// </summary>
/// <value>The start index.</value>
public int? StartIndex { get; set; }
/// <summary>
- /// The maximum number of items to return.
+ /// Gets or sets the maximum number of items to return.
/// </summary>
/// <value>The limit.</value>
public int? Limit { get; set; }
@@ -68,15 +74,15 @@ namespace MediaBrowser.Model.LiveTv
public bool EnableUserData { get; set; }
/// <summary>
- /// Used to specific whether to return news or not.
+ /// Gets or sets a value whether to return news or not.
/// </summary>
- /// <remarks>If set to null, all programs will be returned</remarks>
+ /// <remarks>If set to <c>null</c>, all programs will be returned.</remarks>
public bool? IsNews { get; set; }
/// <summary>
- /// Used to specific whether to return movies or not.
+ /// Gets or sets a value whether to return movies or not.
/// </summary>
- /// <remarks>If set to null, all programs will be returned</remarks>
+ /// <remarks>If set to <c>null</c>, all programs will be returned.</remarks>
public bool? IsMovie { get; set; }
/// <summary>
@@ -96,15 +102,9 @@ namespace MediaBrowser.Model.LiveTv
public string[] SortBy { get; set; }
/// <summary>
- /// The sort order to return results with.
+ /// Gets or sets the sort order to return results with.
/// </summary>
/// <value>The sort order.</value>
public SortOrder? SortOrder { get; set; }
-
- public LiveTvChannelQuery()
- {
- EnableUserData = true;
- SortBy = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs
index 789de3198..4cece941c 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs
@@ -2,12 +2,19 @@
#pragma warning disable CS1591
using System;
-using MediaBrowser.Model.Dto;
namespace MediaBrowser.Model.LiveTv
{
public class LiveTvOptions
{
+ public LiveTvOptions()
+ {
+ TunerHosts = Array.Empty<TunerHostInfo>();
+ ListingProviders = Array.Empty<ListingsProviderInfo>();
+ MediaLocationsCreated = Array.Empty<string>();
+ RecordingPostProcessorArguments = "\"{path}\"";
+ }
+
public int? GuideDays { get; set; }
public string RecordingPath { get; set; }
@@ -33,93 +40,5 @@ namespace MediaBrowser.Model.LiveTv
public string RecordingPostProcessor { get; set; }
public string RecordingPostProcessorArguments { get; set; }
-
- public LiveTvOptions()
- {
- TunerHosts = Array.Empty<TunerHostInfo>();
- ListingProviders = Array.Empty<ListingsProviderInfo>();
- MediaLocationsCreated = Array.Empty<string>();
- RecordingPostProcessorArguments = "\"{path}\"";
- }
- }
-
- public class TunerHostInfo
- {
- public string Id { get; set; }
-
- public string Url { get; set; }
-
- public string Type { get; set; }
-
- public string DeviceId { get; set; }
-
- public string FriendlyName { get; set; }
-
- public bool ImportFavoritesOnly { get; set; }
-
- public bool AllowHWTranscoding { get; set; }
-
- public bool EnableStreamLooping { get; set; }
-
- public string Source { get; set; }
-
- public int TunerCount { get; set; }
-
- public string UserAgent { get; set; }
-
- public TunerHostInfo()
- {
- AllowHWTranscoding = true;
- }
- }
-
- public class ListingsProviderInfo
- {
- public string Id { get; set; }
-
- public string Type { get; set; }
-
- public string Username { get; set; }
-
- public string Password { get; set; }
-
- public string ListingsId { get; set; }
-
- public string ZipCode { get; set; }
-
- public string Country { get; set; }
-
- public string Path { get; set; }
-
- public string[] EnabledTuners { get; set; }
-
- public bool EnableAllTuners { get; set; }
-
- public string[] NewsCategories { get; set; }
-
- public string[] SportsCategories { get; set; }
-
- public string[] KidsCategories { get; set; }
-
- public string[] MovieCategories { get; set; }
-
- public NameValuePair[] ChannelMappings { get; set; }
-
- public string MoviePrefix { get; set; }
-
- public string PreferredLanguage { get; set; }
-
- public string UserAgent { get; set; }
-
- public ListingsProviderInfo()
- {
- NewsCategories = new[] { "news", "journalism", "documentary", "current affairs" };
- SportsCategories = new[] { "sports", "basketball", "baseball", "football" };
- KidsCategories = new[] { "kids", "family", "children", "childrens", "disney" };
- MovieCategories = new[] { "movie" };
- EnabledTuners = Array.Empty<string>();
- EnableAllTuners = true;
- ChannelMappings = Array.Empty<NameValuePair>();
- }
}
}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs
index 856f638c5..ef5c5d2f3 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs
@@ -10,6 +10,11 @@ namespace MediaBrowser.Model.LiveTv
/// </summary>
public class LiveTvServiceInfo
{
+ public LiveTvServiceInfo()
+ {
+ Tuners = Array.Empty<string>();
+ }
+
/// <summary>
/// Gets or sets the name.
/// </summary>
@@ -53,10 +58,5 @@ namespace MediaBrowser.Model.LiveTv
public bool IsVisible { get; set; }
public string[] Tuners { get; set; }
-
- public LiveTvServiceInfo()
- {
- Tuners = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Model/LiveTv/RecordingQuery.cs b/MediaBrowser.Model/LiveTv/RecordingQuery.cs
index 69e7db470..99bb1603c 100644
--- a/MediaBrowser.Model/LiveTv/RecordingQuery.cs
+++ b/MediaBrowser.Model/LiveTv/RecordingQuery.cs
@@ -12,6 +12,11 @@ namespace MediaBrowser.Model.LiveTv
/// </summary>
public class RecordingQuery
{
+ public RecordingQuery()
+ {
+ EnableTotalRecordCount = true;
+ }
+
/// <summary>
/// Gets or sets the channel identifier.
/// </summary>
@@ -31,13 +36,13 @@ namespace MediaBrowser.Model.LiveTv
public string Id { get; set; }
/// <summary>
- /// Skips over a given number of items within the results. Use for paging.
+ /// Gets or sets the start index. Use for paging.
/// </summary>
/// <value>The start index.</value>
public int? StartIndex { get; set; }
/// <summary>
- /// The maximum number of items to return.
+ /// Gets or sets the maximum number of items to return.
/// </summary>
/// <value>The limit.</value>
public int? Limit { get; set; }
@@ -61,7 +66,7 @@ namespace MediaBrowser.Model.LiveTv
public string SeriesTimerId { get; set; }
/// <summary>
- /// Fields to return within the items, in addition to basic information.
+ /// Gets or sets the fields to return within the items, in addition to basic information.
/// </summary>
/// <value>The fields.</value>
public ItemFields[] Fields { get; set; }
@@ -85,10 +90,5 @@ namespace MediaBrowser.Model.LiveTv
public ImageType[] EnableImageTypes { get; set; }
public bool EnableTotalRecordCount { get; set; }
-
- public RecordingQuery()
- {
- EnableTotalRecordCount = true;
- }
}
}
diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs
index 90422d19c..b26f5f45f 100644
--- a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs
+++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs
@@ -7,6 +7,14 @@ using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.LiveTv
{
+ public enum KeepUntil
+ {
+ UntilDeleted,
+ UntilSpaceNeeded,
+ UntilWatched,
+ UntilDate
+ }
+
/// <summary>
/// Class SeriesTimerInfoDto.
/// </summary>
@@ -83,12 +91,4 @@ namespace MediaBrowser.Model.LiveTv
/// <value>The parent primary image tag.</value>
public string ParentPrimaryImageTag { get; set; }
}
-
- public enum KeepUntil
- {
- UntilDeleted,
- UntilSpaceNeeded,
- UntilWatched,
- UntilDate
- }
}
diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
new file mode 100644
index 000000000..05576a0f8
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
@@ -0,0 +1,35 @@
+#nullable disable
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class TunerHostInfo
+ {
+ public TunerHostInfo()
+ {
+ AllowHWTranscoding = true;
+ }
+
+ public string Id { get; set; }
+
+ public string Url { get; set; }
+
+ public string Type { get; set; }
+
+ public string DeviceId { get; set; }
+
+ public string FriendlyName { get; set; }
+
+ public bool ImportFavoritesOnly { get; set; }
+
+ public bool AllowHWTranscoding { get; set; }
+
+ public bool EnableStreamLooping { get; set; }
+
+ public string Source { get; set; }
+
+ public int TunerCount { get; set; }
+
+ public string UserAgent { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index c53428651..4db99f0b0 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -17,9 +17,10 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
- <LangVersion>latest</LangVersion>
+ <!-- <AnalysisMode>AllEnabledByDefault</AnalysisMode> -->
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
@@ -35,7 +36,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
- <PackageReference Include="System.Text.Json" Version="5.0.1" />
+ <PackageReference Include="System.Text.Json" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
@@ -44,7 +45,6 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -53,8 +53,4 @@
<ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/MediaBrowser.Model/MediaInfo/MediaInfo.cs b/MediaBrowser.Model/MediaInfo/MediaInfo.cs
index 472055c22..453aeb028 100644
--- a/MediaBrowser.Model/MediaInfo/MediaInfo.cs
+++ b/MediaBrowser.Model/MediaInfo/MediaInfo.cs
@@ -10,6 +10,17 @@ namespace MediaBrowser.Model.MediaInfo
{
public class MediaInfo : MediaSourceInfo, IHasProviderIds
{
+ public MediaInfo()
+ {
+ Chapters = Array.Empty<ChapterInfo>();
+ Artists = Array.Empty<string>();
+ AlbumArtists = Array.Empty<string>();
+ Studios = Array.Empty<string>();
+ Genres = Array.Empty<string>();
+ People = Array.Empty<BaseItemPerson>();
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
public ChapterInfo[] Chapters { get; set; }
/// <summary>
@@ -40,6 +51,8 @@ namespace MediaBrowser.Model.MediaInfo
public string ShowName { get; set; }
+ public string ForcedSortName { get; set; }
+
public int? IndexNumber { get; set; }
public int? ParentIndexNumber { get; set; }
@@ -69,16 +82,5 @@ namespace MediaBrowser.Model.MediaInfo
/// </summary>
/// <value>The overview.</value>
public string Overview { get; set; }
-
- public MediaInfo()
- {
- Chapters = Array.Empty<ChapterInfo>();
- Artists = Array.Empty<string>();
- AlbumArtists = Array.Empty<string>();
- Studios = Array.Empty<string>();
- Genres = Array.Empty<string>();
- People = Array.Empty<BaseItemPerson>();
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
index 321685677..ecd9b8834 100644
--- a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
+++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
@@ -8,6 +8,17 @@ namespace MediaBrowser.Model.MediaInfo
{
public class PlaybackInfoRequest
{
+ public PlaybackInfoRequest()
+ {
+ EnableDirectPlay = true;
+ EnableDirectStream = true;
+ EnableTranscoding = true;
+ AllowVideoStreamCopy = true;
+ AllowAudioStreamCopy = true;
+ IsPlayback = true;
+ DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
+ }
+
public Guid Id { get; set; }
public Guid UserId { get; set; }
@@ -43,16 +54,5 @@ namespace MediaBrowser.Model.MediaInfo
public bool AutoOpenLiveStream { get; set; }
public MediaProtocol[] DirectPlayProtocols { get; set; }
-
- public PlaybackInfoRequest()
- {
- EnableDirectPlay = true;
- EnableDirectStream = true;
- EnableTranscoding = true;
- AllowVideoStreamCopy = true;
- AllowAudioStreamCopy = true;
- IsPlayback = true;
- DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
- }
}
}
diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
index 273350182..32971b108 100644
--- a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
+++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
@@ -11,6 +11,14 @@ namespace MediaBrowser.Model.MediaInfo
public class PlaybackInfoResponse
{
/// <summary>
+ /// Initializes a new instance of the <see cref="PlaybackInfoResponse" /> class.
+ /// </summary>
+ public PlaybackInfoResponse()
+ {
+ MediaSources = Array.Empty<MediaSourceInfo>();
+ }
+
+ /// <summary>
/// Gets or sets the media sources.
/// </summary>
/// <value>The media sources.</value>
@@ -27,13 +35,5 @@ namespace MediaBrowser.Model.MediaInfo
/// </summary>
/// <value>The error code.</value>
public PlaybackErrorCode? ErrorCode { get; set; }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PlaybackInfoResponse" /> class.
- /// </summary>
- public PlaybackInfoResponse()
- {
- MediaSources = Array.Empty<MediaSourceInfo>();
- }
}
}
diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs
index 72bb3d9c6..88b00c166 100644
--- a/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs
+++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs
@@ -1,10 +1,15 @@
-#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Model.MediaInfo
{
public class SubtitleTrackEvent
{
+ public SubtitleTrackEvent(string id, string text)
+ {
+ Id = id;
+ Text = text;
+ }
+
public string Id { get; set; }
public string Text { get; set; }
diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
index 37f5c55da..d5c3a6aec 100644
--- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
+++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
@@ -7,11 +7,11 @@ namespace MediaBrowser.Model.MediaInfo
{
public class SubtitleTrackInfo
{
- public IReadOnlyList<SubtitleTrackEvent> TrackEvents { get; set; }
-
public SubtitleTrackInfo()
{
TrackEvents = Array.Empty<SubtitleTrackEvent>();
}
+
+ public IReadOnlyList<SubtitleTrackEvent> TrackEvents { get; set; }
}
}
diff --git a/MediaBrowser.Model/Net/ISocket.cs b/MediaBrowser.Model/Net/ISocket.cs
index 5b6ed92df..3de41d565 100644
--- a/MediaBrowser.Model/Net/ISocket.cs
+++ b/MediaBrowser.Model/Net/ISocket.cs
@@ -23,6 +23,12 @@ namespace MediaBrowser.Model.Net
/// <summary>
/// Sends a UDP message to a particular end point (uni or multicast).
/// </summary>
+ /// <param name="buffer">An array of type <see cref="byte" /> that contains the data to send.</param>
+ /// <param name="offset">The zero-based position in buffer at which to begin sending data.</param>
+ /// <param name="bytes">The number of bytes to send.</param>
+ /// <param name="endPoint">An <see cref="IPEndPoint" /> that represents the remote device.</param>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>The task object representing the asynchronous operation.</returns>
Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs
index 363abefc1..1527ef595 100644
--- a/MediaBrowser.Model/Net/ISocketFactory.cs
+++ b/MediaBrowser.Model/Net/ISocketFactory.cs
@@ -14,6 +14,9 @@ namespace MediaBrowser.Model.Net
/// <summary>
/// Creates a new unicast socket using the specified local port number.
/// </summary>
+ /// <param name="localIp">The local IP address to bind to.</param>
+ /// <param name="localPort">The local port to bind to.</param>
+ /// <returns>A new unicast socket using the specified local port number.</returns>
ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort);
/// <summary>
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 902db1e9e..96f5ab51a 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -91,9 +91,9 @@ namespace MediaBrowser.Model.Net
{ ".webp", "image/webp" },
// Type font
- { ".ttf" , "font/ttf" },
- { ".woff" , "font/woff" },
- { ".woff2" , "font/woff2" },
+ { ".ttf", "font/ttf" },
+ { ".woff", "font/woff" },
+ { ".woff2", "font/woff2" },
// Type text
{ ".ass", "text/x-ssa" },
@@ -168,14 +168,17 @@ namespace MediaBrowser.Model.Net
/// <summary>
/// Gets the type of the MIME.
/// </summary>
- public static string? GetMimeType(string path, bool enableStreamDefault)
+ /// <param name="filename">The filename to find the MIME type of.</param>
+ /// <param name="enableStreamDefault">Whether of not to return a default value if no fitting MIME type is found.</param>
+ /// <returns>The worrect MIME type for the given filename, or `null` if it wasn't found and <paramref name="enableStreamDefault"/> is false.</returns>
+ public static string? GetMimeType(string filename, bool enableStreamDefault)
{
- if (path.Length == 0)
+ if (filename.Length == 0)
{
- throw new ArgumentException("String can't be empty.", nameof(path));
+ throw new ArgumentException("String can't be empty.", nameof(filename));
}
- var ext = Path.GetExtension(path);
+ var ext = Path.GetExtension(filename);
if (_mimeTypeLookup.TryGetValue(ext, out string? result))
{
@@ -210,9 +213,9 @@ namespace MediaBrowser.Model.Net
return enableStreamDefault ? "application/octet-stream" : null;
}
- public static string? ToExtension(string? mimeType)
+ public static string? ToExtension(string mimeType)
{
- if (string.IsNullOrEmpty(mimeType))
+ if (mimeType.Length == 0)
{
throw new ArgumentException("String can't be empty.", nameof(mimeType));
}
diff --git a/MediaBrowser.Model/Net/NetworkShare.cs b/MediaBrowser.Model/Net/NetworkShare.cs
deleted file mode 100644
index 6344cbe21..000000000
--- a/MediaBrowser.Model/Net/NetworkShare.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Net
-{
- public class NetworkShare
- {
- /// <summary>
- /// The name of the computer that this share belongs to.
- /// </summary>
- public string Server { get; set; }
-
- /// <summary>
- /// Share name.
- /// </summary>
- public string Name { get; set; }
-
- /// <summary>
- /// Local path.
- /// </summary>
- public string Path { get; set; }
-
- /// <summary>
- /// Share type.
- /// </summary>
- public NetworkShareType ShareType { get; set; }
-
- /// <summary>
- /// Comment.
- /// </summary>
- public string Remark { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Net/SocketReceiveResult.cs b/MediaBrowser.Model/Net/SocketReceiveResult.cs
index 54139fe9c..1524786ea 100644
--- a/MediaBrowser.Model/Net/SocketReceiveResult.cs
+++ b/MediaBrowser.Model/Net/SocketReceiveResult.cs
@@ -20,12 +20,12 @@ namespace MediaBrowser.Model.Net
public int ReceivedBytes { get; set; }
/// <summary>
- /// The <see cref="IPEndPoint"/> the data was received from.
+ /// Gets or sets the <see cref="IPEndPoint"/> the data was received from.
/// </summary>
public IPEndPoint RemoteEndPoint { get; set; }
/// <summary>
- /// The local <see cref="IPAddress"/>.
+ /// Gets or sets the local <see cref="IPAddress"/>.
/// </summary>
public IPAddress LocalIPAddress { get; set; }
}
diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs
index bffbbe612..b00158cb3 100644
--- a/MediaBrowser.Model/Net/WebSocketMessage.cs
+++ b/MediaBrowser.Model/Net/WebSocketMessage.cs
@@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Net
/// <summary>
/// Class WebSocketMessage.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of the data.</typeparam>
public class WebSocketMessage<T>
{
/// <summary>
diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs
index 239a3777e..09beb2ef7 100644
--- a/MediaBrowser.Model/Notifications/NotificationOptions.cs
+++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs
@@ -2,18 +2,14 @@
#pragma warning disable CS1591
using System;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Model.Extensions;
using System.Linq;
using Jellyfin.Data.Entities;
-using MediaBrowser.Model.Users;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Notifications
{
public class NotificationOptions
{
- public NotificationOption[] Options { get; set; }
-
public NotificationOptions()
{
Options = new[]
@@ -71,6 +67,8 @@ namespace MediaBrowser.Model.Notifications
};
}
+ public NotificationOption[] Options { get; set; }
+
public NotificationOption GetOptions(string type)
{
foreach (NotificationOption i in Options)
@@ -95,16 +93,17 @@ namespace MediaBrowser.Model.Notifications
{
NotificationOption opt = GetOptions(notificationType);
- return opt == null ||
- !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase);
+ return opt == null
+ || !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase);
}
public bool IsEnabledToMonitorUser(string type, Guid userId)
{
NotificationOption opt = GetOptions(type);
- return opt != null && opt.Enabled &&
- !opt.DisabledMonitorUsers.Contains(userId.ToString(""), StringComparer.OrdinalIgnoreCase);
+ return opt != null
+ && opt.Enabled
+ && !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparer.OrdinalIgnoreCase);
}
public bool IsEnabledToSendToUser(string type, string userId, User user)
diff --git a/MediaBrowser.Model/Notifications/NotificationRequest.cs b/MediaBrowser.Model/Notifications/NotificationRequest.cs
index febc2bc09..622c50cd8 100644
--- a/MediaBrowser.Model/Notifications/NotificationRequest.cs
+++ b/MediaBrowser.Model/Notifications/NotificationRequest.cs
@@ -7,6 +7,12 @@ namespace MediaBrowser.Model.Notifications
{
public class NotificationRequest
{
+ public NotificationRequest()
+ {
+ UserIds = Array.Empty<Guid>();
+ Date = DateTime.UtcNow;
+ }
+
public string Name { get; set; }
public string Description { get; set; }
@@ -20,16 +26,10 @@ namespace MediaBrowser.Model.Notifications
public DateTime Date { get; set; }
/// <summary>
- /// The corresponding type name used in configuration. Not for display.
+ /// Gets or sets the corresponding type name used in configuration. Not for display.
/// </summary>
public string NotificationType { get; set; }
public SendToUserType? SendToUserMode { get; set; }
-
- public NotificationRequest()
- {
- UserIds = Array.Empty<Guid>();
- Date = DateTime.UtcNow;
- }
}
}
diff --git a/MediaBrowser.Model/Plugins/PluginInfo.cs b/MediaBrowser.Model/Plugins/PluginInfo.cs
index 25216610d..8eb90bdb0 100644
--- a/MediaBrowser.Model/Plugins/PluginInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
namespace MediaBrowser.Model.Plugins
diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
index 85c0aa204..f4d83c28b 100644
--- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
namespace MediaBrowser.Model.Plugins
{
/// <summary>
diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
index afe95e6ee..0ea3e96ca 100644
--- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs
+++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
@@ -6,11 +6,11 @@ namespace MediaBrowser.Model.Providers
public class ExternalIdInfo
{
/// <summary>
- /// Represents the external id information for serialization to the client.
+ /// Initializes a new instance of the <see cref="ExternalIdInfo"/> class.
/// </summary>
/// <param name="name">Name of the external id provider (IE: IMDB, MusicBrainz, etc).</param>
/// <param name="key">Key for this id. This key should be unique across all providers.</param>
- /// <param name="type">Specific media type for this id</param>
+ /// <param name="type">Specific media type for this id.</param>
/// <param name="urlFormatString">URL format string.</param>
public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string urlFormatString)
{
diff --git a/MediaBrowser.Model/Providers/RemoteImageInfo.cs b/MediaBrowser.Model/Providers/RemoteImageInfo.cs
index fb25999e0..48207d2d4 100644
--- a/MediaBrowser.Model/Providers/RemoteImageInfo.cs
+++ b/MediaBrowser.Model/Providers/RemoteImageInfo.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Model.Providers
public string Url { get; set; }
/// <summary>
- /// Gets a url used for previewing a smaller version.
+ /// Gets or sets a url used for previewing a smaller version.
/// </summary>
public string ThumbnailUrl { get; set; }
diff --git a/MediaBrowser.Model/Providers/SubtitleOptions.cs b/MediaBrowser.Model/Providers/SubtitleOptions.cs
index 5702c460b..6ea1e1486 100644
--- a/MediaBrowser.Model/Providers/SubtitleOptions.cs
+++ b/MediaBrowser.Model/Providers/SubtitleOptions.cs
@@ -7,6 +7,14 @@ namespace MediaBrowser.Model.Providers
{
public class SubtitleOptions
{
+ public SubtitleOptions()
+ {
+ DownloadLanguages = Array.Empty<string>();
+
+ SkipIfAudioTrackMatches = true;
+ RequirePerfectMatch = true;
+ }
+
public bool SkipIfEmbeddedSubtitlesPresent { get; set; }
public bool SkipIfAudioTrackMatches { get; set; }
@@ -24,13 +32,5 @@ namespace MediaBrowser.Model.Providers
public bool IsOpenSubtitleVipAccount { get; set; }
public bool RequirePerfectMatch { get; set; }
-
- public SubtitleOptions()
- {
- DownloadLanguages = Array.Empty<string>();
-
- SkipIfAudioTrackMatches = true;
- RequirePerfectMatch = true;
- }
}
}
diff --git a/MediaBrowser.Model/Querying/EpisodeQuery.cs b/MediaBrowser.Model/Querying/EpisodeQuery.cs
deleted file mode 100644
index 13b1a0dcb..000000000
--- a/MediaBrowser.Model/Querying/EpisodeQuery.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Querying
-{
- public class EpisodeQuery
- {
- /// <summary>
- /// Gets or sets the user identifier.
- /// </summary>
- /// <value>The user identifier.</value>
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the season identifier.
- /// </summary>
- /// <value>The season identifier.</value>
- public string SeasonId { get; set; }
-
- /// <summary>
- /// Gets or sets the series identifier.
- /// </summary>
- /// <value>The series identifier.</value>
- public string SeriesId { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is missing.
- /// </summary>
- /// <value><c>null</c> if [is missing] contains no value, <c>true</c> if [is missing]; otherwise, <c>false</c>.</value>
- public bool? IsMissing { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is virtual unaired.
- /// </summary>
- /// <value><c>null</c> if [is virtual unaired] contains no value, <c>true</c> if [is virtual unaired]; otherwise, <c>false</c>.</value>
- public bool? IsVirtualUnaired { get; set; }
-
- /// <summary>
- /// Gets or sets the season number.
- /// </summary>
- /// <value>The season number.</value>
- public int? SeasonNumber { get; set; }
-
- /// <summary>
- /// Gets or sets the fields.
- /// </summary>
- /// <value>The fields.</value>
- public ItemFields[] Fields { get; set; }
-
- /// <summary>
- /// Gets or sets the start index.
- /// </summary>
- /// <value>The start index.</value>
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the limit.
- /// </summary>
- /// <value>The limit.</value>
- public int? Limit { get; set; }
-
- /// <summary>
- /// Gets or sets the start item identifier.
- /// </summary>
- /// <value>The start item identifier.</value>
- public string StartItemId { get; set; }
-
- public EpisodeQuery()
- {
- Fields = Array.Empty<ItemFields>();
- }
- }
-}
diff --git a/MediaBrowser.Model/Querying/LatestItemsQuery.cs b/MediaBrowser.Model/Querying/LatestItemsQuery.cs
index 7954ef4b4..f555ffb36 100644
--- a/MediaBrowser.Model/Querying/LatestItemsQuery.cs
+++ b/MediaBrowser.Model/Querying/LatestItemsQuery.cs
@@ -14,31 +14,32 @@ namespace MediaBrowser.Model.Querying
}
/// <summary>
- /// The user to localize search results for.
+ /// Gets or sets the user to localize search results for.
/// </summary>
/// <value>The user id.</value>
public Guid UserId { get; set; }
/// <summary>
+ /// Gets or sets the parent id.
/// Specify this to localize the search to a specific item or folder. Omit to use the root.
/// </summary>
/// <value>The parent id.</value>
public Guid ParentId { get; set; }
/// <summary>
- /// Skips over a given number of items within the results. Use for paging.
+ /// Gets or sets the start index. Used for paging.
/// </summary>
/// <value>The start index.</value>
public int? StartIndex { get; set; }
/// <summary>
- /// The maximum number of items to return.
+ /// Gets or sets the maximum number of items to return.
/// </summary>
/// <value>The limit.</value>
public int? Limit { get; set; }
/// <summary>
- /// Fields to return within the items, in addition to basic information.
+ /// Gets or sets the fields to return within the items, in addition to basic information.
/// </summary>
/// <value>The fields.</value>
public ItemFields[] Fields { get; set; }
diff --git a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs
deleted file mode 100644
index 1c8875890..000000000
--- a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Querying
-{
- public class MovieRecommendationQuery
- {
- /// <summary>
- /// Gets or sets the user identifier.
- /// </summary>
- /// <value>The user identifier.</value>
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the parent identifier.
- /// </summary>
- /// <value>The parent identifier.</value>
- public string ParentId { get; set; }
-
- /// <summary>
- /// Gets or sets the item limit.
- /// </summary>
- /// <value>The item limit.</value>
- public int ItemLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the category limit.
- /// </summary>
- /// <value>The category limit.</value>
- public int CategoryLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the fields.
- /// </summary>
- /// <value>The fields.</value>
- public ItemFields[] Fields { get; set; }
-
- public MovieRecommendationQuery()
- {
- ItemLimit = 10;
- CategoryLimit = 6;
- Fields = Array.Empty<ItemFields>();
- }
- }
-}
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 001d0623c..fa8aa829d 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -8,6 +8,14 @@ namespace MediaBrowser.Model.Querying
{
public class NextUpQuery
{
+ public NextUpQuery()
+ {
+ EnableImageTypes = Array.Empty<ImageType>();
+ EnableTotalRecordCount = true;
+ DisableFirstEpisode = false;
+ NextUpDateCutoff = DateTime.MinValue;
+ }
+
/// <summary>
/// Gets or sets the user id.
/// </summary>
@@ -27,19 +35,19 @@ namespace MediaBrowser.Model.Querying
public string SeriesId { get; set; }
/// <summary>
- /// Skips over a given number of items within the results. Use for paging.
+ /// Gets or sets the start index. Use for paging.
/// </summary>
/// <value>The start index.</value>
public int? StartIndex { get; set; }
/// <summary>
- /// The maximum number of items to return.
+ /// Gets or sets the maximum number of items to return.
/// </summary>
/// <value>The limit.</value>
public int? Limit { get; set; }
/// <summary>
- /// Fields to return within the items, in addition to basic information.
+ /// gets or sets the fields to return within the items, in addition to basic information.
/// </summary>
/// <value>The fields.</value>
public ItemFields[] Fields { get; set; }
@@ -69,11 +77,9 @@ namespace MediaBrowser.Model.Querying
/// </summary>
public bool DisableFirstEpisode { get; set; }
- public NextUpQuery()
- {
- EnableImageTypes = Array.Empty<ImageType>();
- EnableTotalRecordCount = true;
- DisableFirstEpisode = false;
- }
+ /// <summary>
+ /// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
+ /// </summary>
+ public DateTime NextUpDateCutoff { get; set; }
}
}
diff --git a/MediaBrowser.Model/Querying/QueryFilters.cs b/MediaBrowser.Model/Querying/QueryFilters.cs
index 6e4d25181..73b27a7b0 100644
--- a/MediaBrowser.Model/Querying/QueryFilters.cs
+++ b/MediaBrowser.Model/Querying/QueryFilters.cs
@@ -6,35 +6,16 @@ using MediaBrowser.Model.Dto;
namespace MediaBrowser.Model.Querying
{
- public class QueryFiltersLegacy
+ public class QueryFilters
{
- public string[] Genres { get; set; }
-
- public string[] Tags { get; set; }
-
- public string[] OfficialRatings { get; set; }
-
- public int[] Years { get; set; }
-
- public QueryFiltersLegacy()
+ public QueryFilters()
{
- Genres = Array.Empty<string>();
Tags = Array.Empty<string>();
- OfficialRatings = Array.Empty<string>();
- Years = Array.Empty<int>();
+ Genres = Array.Empty<NameGuidPair>();
}
- }
- public class QueryFilters
- {
public NameGuidPair[] Genres { get; set; }
public string[] Tags { get; set; }
-
- public QueryFilters()
- {
- Tags = Array.Empty<string>();
- Genres = Array.Empty<NameGuidPair>();
- }
}
}
diff --git a/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs b/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs
new file mode 100644
index 000000000..fcb450ed3
--- /dev/null
+++ b/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs
@@ -0,0 +1,26 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Model.Querying
+{
+ public class QueryFiltersLegacy
+ {
+ public QueryFiltersLegacy()
+ {
+ Genres = Array.Empty<string>();
+ Tags = Array.Empty<string>();
+ OfficialRatings = Array.Empty<string>();
+ Years = Array.Empty<int>();
+ }
+
+ public string[] Genres { get; set; }
+
+ public string[] Tags { get; set; }
+
+ public string[] OfficialRatings { get; set; }
+
+ public int[] Years { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs
index 490f48b84..8ce794800 100644
--- a/MediaBrowser.Model/Querying/QueryResult.cs
+++ b/MediaBrowser.Model/Querying/QueryResult.cs
@@ -8,6 +8,17 @@ namespace MediaBrowser.Model.Querying
{
public class QueryResult<T>
{
+ public QueryResult()
+ {
+ Items = Array.Empty<T>();
+ }
+
+ public QueryResult(IReadOnlyList<T> items)
+ {
+ Items = items;
+ TotalRecordCount = items.Count;
+ }
+
/// <summary>
/// Gets or sets the items.
/// </summary>
@@ -15,26 +26,15 @@ namespace MediaBrowser.Model.Querying
public IReadOnlyList<T> Items { get; set; }
/// <summary>
- /// The total number of records available.
+ /// Gets or sets the total number of records available.
/// </summary>
/// <value>The total record count.</value>
public int TotalRecordCount { get; set; }
/// <summary>
- /// The index of the first record in Items.
+ /// Gets or sets the index of the first record in Items.
/// </summary>
/// <value>First record index.</value>
public int StartIndex { get; set; }
-
- public QueryResult()
- {
- Items = Array.Empty<T>();
- }
-
- public QueryResult(IReadOnlyList<T> items)
- {
- Items = items;
- TotalRecordCount = items.Count;
- }
}
}
diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
deleted file mode 100644
index eb6239460..000000000
--- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Model.Querying
-{
- public class UpcomingEpisodesQuery
- {
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the parent identifier.
- /// </summary>
- /// <value>The parent identifier.</value>
- public string ParentId { get; set; }
-
- /// <summary>
- /// Skips over a given number of items within the results. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// The maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- public int? Limit { get; set; }
-
- /// <summary>
- /// Fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- public ItemFields[] Fields { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [enable images].
- /// </summary>
- /// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value>
- public bool? EnableImages { get; set; }
-
- /// <summary>
- /// Gets or sets the image type limit.
- /// </summary>
- /// <value>The image type limit.</value>
- public int? ImageTypeLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the enable image types.
- /// </summary>
- /// <value>The enable image types.</value>
- public ImageType[] EnableImageTypes { get; set; }
-
- public UpcomingEpisodesQuery()
- {
- EnableImageTypes = Array.Empty<ImageType>();
- }
- }
-}
diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs
index ce60062cd..aedfa4d36 100644
--- a/MediaBrowser.Model/Search/SearchQuery.cs
+++ b/MediaBrowser.Model/Search/SearchQuery.cs
@@ -7,8 +7,21 @@ namespace MediaBrowser.Model.Search
{
public class SearchQuery
{
+ public SearchQuery()
+ {
+ IncludeArtists = true;
+ IncludeGenres = true;
+ IncludeMedia = true;
+ IncludePeople = true;
+ IncludeStudios = true;
+
+ MediaTypes = Array.Empty<string>();
+ IncludeItemTypes = Array.Empty<string>();
+ ExcludeItemTypes = Array.Empty<string>();
+ }
+
/// <summary>
- /// The user to localize search results for.
+ /// Gets or sets the user to localize search results for.
/// </summary>
/// <value>The user id.</value>
public Guid UserId { get; set; }
@@ -20,13 +33,13 @@ namespace MediaBrowser.Model.Search
public string SearchTerm { get; set; }
/// <summary>
- /// Skips over a given number of items within the results. Use for paging.
+ /// Gets or sets the start index. Used for paging.
/// </summary>
/// <value>The start index.</value>
public int? StartIndex { get; set; }
/// <summary>
- /// The maximum number of items to return.
+ /// Gets or sets the maximum number of items to return.
/// </summary>
/// <value>The limit.</value>
public int? Limit { get; set; }
@@ -58,18 +71,5 @@ namespace MediaBrowser.Model.Search
public bool? IsKids { get; set; }
public bool? IsSports { get; set; }
-
- public SearchQuery()
- {
- IncludeArtists = true;
- IncludeGenres = true;
- IncludeMedia = true;
- IncludePeople = true;
- IncludeStudios = true;
-
- MediaTypes = Array.Empty<string>();
- IncludeItemTypes = Array.Empty<string>();
- ExcludeItemTypes = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Model/Session/BrowseRequest.cs b/MediaBrowser.Model/Session/BrowseRequest.cs
index 1c997d584..65afe5cf3 100644
--- a/MediaBrowser.Model/Session/BrowseRequest.cs
+++ b/MediaBrowser.Model/Session/BrowseRequest.cs
@@ -7,6 +7,7 @@ namespace MediaBrowser.Model.Session
public class BrowseRequest
{
/// <summary>
+ /// Gets or sets the item type.
/// Artist, Genre, Studio, Person, or any kind of BaseItem.
/// </summary>
/// <value>The type of the item.</value>
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
index 5852f4e37..d692906c6 100644
--- a/MediaBrowser.Model/Session/ClientCapabilities.cs
+++ b/MediaBrowser.Model/Session/ClientCapabilities.cs
@@ -9,6 +9,13 @@ namespace MediaBrowser.Model.Session
{
public class ClientCapabilities
{
+ public ClientCapabilities()
+ {
+ PlayableMediaTypes = Array.Empty<string>();
+ SupportedCommands = Array.Empty<GeneralCommandType>();
+ SupportsPersistentIdentifier = true;
+ }
+
public IReadOnlyList<string> PlayableMediaTypes { get; set; }
public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; }
@@ -28,12 +35,5 @@ namespace MediaBrowser.Model.Session
public string AppStoreUrl { get; set; }
public string IconUrl { get; set; }
-
- public ClientCapabilities()
- {
- PlayableMediaTypes = Array.Empty<string>();
- SupportedCommands = Array.Empty<GeneralCommandType>();
- SupportsPersistentIdentifier = true;
- }
}
}
diff --git a/MediaBrowser.Model/Session/GeneralCommand.cs b/MediaBrowser.Model/Session/GeneralCommand.cs
index 77bb6bcf7..29528c110 100644
--- a/MediaBrowser.Model/Session/GeneralCommand.cs
+++ b/MediaBrowser.Model/Session/GeneralCommand.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -8,15 +7,15 @@ namespace MediaBrowser.Model.Session
{
public class GeneralCommand
{
- public GeneralCommandType Name { get; set; }
-
- public Guid ControllingUserId { get; set; }
-
- public Dictionary<string, string> Arguments { get; set; }
-
public GeneralCommand()
{
Arguments = new Dictionary<string, string>();
}
+
+ public GeneralCommandType Name { get; set; }
+
+ public Guid ControllingUserId { get; set; }
+
+ public Dictionary<string, string> Arguments { get; }
}
}
diff --git a/MediaBrowser.Model/Session/MessageCommand.cs b/MediaBrowser.Model/Session/MessageCommand.cs
index 09abfbb3f..cc9db8e6c 100644
--- a/MediaBrowser.Model/Session/MessageCommand.cs
+++ b/MediaBrowser.Model/Session/MessageCommand.cs
@@ -1,12 +1,15 @@
#nullable disable
#pragma warning disable CS1591
+using System.ComponentModel.DataAnnotations;
+
namespace MediaBrowser.Model.Session
{
public class MessageCommand
{
public string Header { get; set; }
+ [Required(AllowEmptyStrings = false)]
public string Text { get; set; }
public long? TimeoutMs { get; set; }
diff --git a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs
index 73dbe6a2d..a6e7efcb0 100644
--- a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs
+++ b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs
@@ -111,18 +111,4 @@ namespace MediaBrowser.Model.Session
public string PlaylistItemId { get; set; }
}
-
- public enum RepeatMode
- {
- RepeatNone = 0,
- RepeatAll = 1,
- RepeatOne = 2
- }
-
- public class QueueItem
- {
- public Guid Id { get; set; }
-
- public string PlaylistItemId { get; set; }
- }
}
diff --git a/MediaBrowser.Model/Session/PlaystateCommand.cs b/MediaBrowser.Model/Session/PlaystateCommand.cs
index 3aa091f79..df47f3b73 100644
--- a/MediaBrowser.Model/Session/PlaystateCommand.cs
+++ b/MediaBrowser.Model/Session/PlaystateCommand.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
namespace MediaBrowser.Model.Session
{
/// <summary>
@@ -46,6 +44,10 @@ namespace MediaBrowser.Model.Session
/// The fast forward.
/// </summary>
FastForward,
+
+ /// <summary>
+ /// The play pause.
+ /// </summary>
PlayPause
}
}
diff --git a/MediaBrowser.Model/Session/QueueItem.cs b/MediaBrowser.Model/Session/QueueItem.cs
new file mode 100644
index 000000000..32b19101b
--- /dev/null
+++ b/MediaBrowser.Model/Session/QueueItem.cs
@@ -0,0 +1,14 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Model.Session
+{
+ public class QueueItem
+ {
+ public Guid Id { get; set; }
+
+ public string PlaylistItemId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Session/RepeatMode.cs b/MediaBrowser.Model/Session/RepeatMode.cs
new file mode 100644
index 000000000..c6e173d6b
--- /dev/null
+++ b/MediaBrowser.Model/Session/RepeatMode.cs
@@ -0,0 +1,11 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Session
+{
+ public enum RepeatMode
+ {
+ RepeatNone = 0,
+ RepeatAll = 1,
+ RepeatOne = 2
+ }
+}
diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs
new file mode 100644
index 000000000..e93b5d288
--- /dev/null
+++ b/MediaBrowser.Model/Session/TranscodeReason.cs
@@ -0,0 +1,31 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Session
+{
+ public enum TranscodeReason
+ {
+ ContainerNotSupported = 0,
+ VideoCodecNotSupported = 1,
+ AudioCodecNotSupported = 2,
+ ContainerBitrateExceedsLimit = 3,
+ AudioBitrateNotSupported = 4,
+ AudioChannelsNotSupported = 5,
+ VideoResolutionNotSupported = 6,
+ UnknownVideoStreamInfo = 7,
+ UnknownAudioStreamInfo = 8,
+ AudioProfileNotSupported = 9,
+ AudioSampleRateNotSupported = 10,
+ AnamorphicVideoNotSupported = 11,
+ InterlacedVideoNotSupported = 12,
+ SecondaryAudioNotSupported = 13,
+ RefFramesNotSupported = 14,
+ VideoBitDepthNotSupported = 15,
+ VideoBitrateNotSupported = 16,
+ VideoFramerateNotSupported = 17,
+ VideoLevelNotSupported = 18,
+ VideoProfileNotSupported = 19,
+ AudioBitDepthNotSupported = 20,
+ SubtitleCodecNotSupported = 21,
+ DirectPlayError = 22
+ }
+}
diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs
index e832c2f6f..064a087d5 100644
--- a/MediaBrowser.Model/Session/TranscodingInfo.cs
+++ b/MediaBrowser.Model/Session/TranscodingInfo.cs
@@ -7,6 +7,11 @@ namespace MediaBrowser.Model.Session
{
public class TranscodingInfo
{
+ public TranscodingInfo()
+ {
+ TranscodeReasons = Array.Empty<TranscodeReason>();
+ }
+
public string AudioCodec { get; set; }
public string VideoCodec { get; set; }
@@ -30,37 +35,5 @@ namespace MediaBrowser.Model.Session
public int? AudioChannels { get; set; }
public TranscodeReason[] TranscodeReasons { get; set; }
-
- public TranscodingInfo()
- {
- TranscodeReasons = Array.Empty<TranscodeReason>();
- }
- }
-
- public enum TranscodeReason
- {
- ContainerNotSupported = 0,
- VideoCodecNotSupported = 1,
- AudioCodecNotSupported = 2,
- ContainerBitrateExceedsLimit = 3,
- AudioBitrateNotSupported = 4,
- AudioChannelsNotSupported = 5,
- VideoResolutionNotSupported = 6,
- UnknownVideoStreamInfo = 7,
- UnknownAudioStreamInfo = 8,
- AudioProfileNotSupported = 9,
- AudioSampleRateNotSupported = 10,
- AnamorphicVideoNotSupported = 11,
- InterlacedVideoNotSupported = 12,
- SecondaryAudioNotSupported = 13,
- RefFramesNotSupported = 14,
- VideoBitDepthNotSupported = 15,
- VideoBitrateNotSupported = 16,
- VideoFramerateNotSupported = 17,
- VideoLevelNotSupported = 18,
- VideoProfileNotSupported = 19,
- AudioBitDepthNotSupported = 20,
- SubtitleCodecNotSupported = 21,
- DirectPlayError = 22
}
}
diff --git a/MediaBrowser.Model/Sync/SyncCategory.cs b/MediaBrowser.Model/Sync/SyncCategory.cs
deleted file mode 100644
index 1248c2f73..000000000
--- a/MediaBrowser.Model/Sync/SyncCategory.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Sync
-{
- public enum SyncCategory
- {
- /// <summary>
- /// The latest.
- /// </summary>
- Latest = 0,
-
- /// <summary>
- /// The next up.
- /// </summary>
- NextUp = 1,
-
- /// <summary>
- /// The resume.
- /// </summary>
- Resume = 2
- }
-}
diff --git a/MediaBrowser.Model/Sync/SyncJob.cs b/MediaBrowser.Model/Sync/SyncJob.cs
deleted file mode 100644
index b9290b6e8..000000000
--- a/MediaBrowser.Model/Sync/SyncJob.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Sync
-{
- public class SyncJob
- {
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the device identifier.
- /// </summary>
- /// <value>The device identifier.</value>
- public string TargetId { get; set; }
-
- /// <summary>
- /// Gets or sets the name of the target.
- /// </summary>
- /// <value>The name of the target.</value>
- public string TargetName { get; set; }
-
- /// <summary>
- /// Gets or sets the quality.
- /// </summary>
- /// <value>The quality.</value>
- public string Quality { get; set; }
-
- /// <summary>
- /// Gets or sets the bitrate.
- /// </summary>
- /// <value>The bitrate.</value>
- public int? Bitrate { get; set; }
-
- /// <summary>
- /// Gets or sets the profile.
- /// </summary>
- /// <value>The profile.</value>
- public string Profile { get; set; }
-
- /// <summary>
- /// Gets or sets the category.
- /// </summary>
- /// <value>The category.</value>
- public SyncCategory? Category { get; set; }
-
- /// <summary>
- /// Gets or sets the parent identifier.
- /// </summary>
- /// <value>The parent identifier.</value>
- public string ParentId { get; set; }
-
- /// <summary>
- /// Gets or sets the current progress.
- /// </summary>
- /// <value>The current progress.</value>
- public double? Progress { get; set; }
-
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the status.
- /// </summary>
- /// <value>The status.</value>
- public SyncJobStatus Status { get; set; }
-
- /// <summary>
- /// Gets or sets the user identifier.
- /// </summary>
- /// <value>The user identifier.</value>
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [unwatched only].
- /// </summary>
- /// <value><c>true</c> if [unwatched only]; otherwise, <c>false</c>.</value>
- public bool UnwatchedOnly { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [synchronize new content].
- /// </summary>
- /// <value><c>true</c> if [synchronize new content]; otherwise, <c>false</c>.</value>
- public bool SyncNewContent { get; set; }
-
- /// <summary>
- /// Gets or sets the item limit.
- /// </summary>
- /// <value>The item limit.</value>
- public int? ItemLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the requested item ids.
- /// </summary>
- /// <value>The requested item ids.</value>
- public Guid[] RequestedItemIds { get; set; }
-
- /// <summary>
- /// Gets or sets the date created.
- /// </summary>
- /// <value>The date created.</value>
- public DateTime DateCreated { get; set; }
-
- /// <summary>
- /// Gets or sets the date last modified.
- /// </summary>
- /// <value>The date last modified.</value>
- public DateTime DateLastModified { get; set; }
-
- /// <summary>
- /// Gets or sets the item count.
- /// </summary>
- /// <value>The item count.</value>
- public int ItemCount { get; set; }
-
- public string ParentName { get; set; }
-
- public string PrimaryImageItemId { get; set; }
-
- public string PrimaryImageTag { get; set; }
-
- public SyncJob()
- {
- RequestedItemIds = Array.Empty<Guid>();
- }
- }
-}
diff --git a/MediaBrowser.Model/Sync/SyncJobStatus.cs b/MediaBrowser.Model/Sync/SyncJobStatus.cs
deleted file mode 100644
index 226a47d4c..000000000
--- a/MediaBrowser.Model/Sync/SyncJobStatus.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Sync
-{
- public enum SyncJobStatus
- {
- Queued = 0,
- Converting = 1,
- ReadyToTransfer = 2,
- Transferring = 3,
- Completed = 4,
- CompletedWithError = 5,
- Failed = 6
- }
-}
diff --git a/MediaBrowser.Model/Sync/SyncTarget.cs b/MediaBrowser.Model/Sync/SyncTarget.cs
deleted file mode 100644
index 9e6bbbc00..000000000
--- a/MediaBrowser.Model/Sync/SyncTarget.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Sync
-{
- public class SyncTarget
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index 4b83fb7e6..d75ae91c0 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -31,13 +31,21 @@ namespace MediaBrowser.Model.System
public class SystemInfo : PublicSystemInfo
{
/// <summary>
+ /// Initializes a new instance of the <see cref="SystemInfo" /> class.
+ /// </summary>
+ public SystemInfo()
+ {
+ CompletedInstallations = Array.Empty<InstallationInfo>();
+ }
+
+ /// <summary>
/// Gets or sets the display name of the operating system.
/// </summary>
/// <value>The display name of the operating system.</value>
public string OperatingSystemDisplayName { get; set; }
/// <summary>
- /// Get or sets the package name.
+ /// Gets or sets the package name.
/// </summary>
/// <value>The value of the '-package' command line argument.</value>
public string PackageName { get; set; }
@@ -127,13 +135,5 @@ namespace MediaBrowser.Model.System
public FFmpegLocation EncoderLocation { get; set; }
public Architecture SystemArchitecture { get; set; }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SystemInfo" /> class.
- /// </summary>
- public SystemInfo()
- {
- CompletedInstallations = Array.Empty<InstallationInfo>();
- }
}
}
diff --git a/MediaBrowser.Model/System/WakeOnLanInfo.cs b/MediaBrowser.Model/System/WakeOnLanInfo.cs
index b2cbe737d..aba19a6ba 100644
--- a/MediaBrowser.Model/System/WakeOnLanInfo.cs
+++ b/MediaBrowser.Model/System/WakeOnLanInfo.cs
@@ -36,7 +36,7 @@ namespace MediaBrowser.Model.System
/// Gets the MAC address of the device.
/// </summary>
/// <value>The MAC address.</value>
- public string? MacAddress { get; set; }
+ public string? MacAddress { get; }
/// <summary>
/// Gets or sets the wake-on-LAN port.
diff --git a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs
index 2f05e08c5..ca769e26b 100644
--- a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs
+++ b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Tasks
event EventHandler<GenericEventArgs<double>> TaskProgress;
/// <summary>
- /// Gets or sets the scheduled task.
+ /// Gets the scheduled task.
/// </summary>
/// <value>The scheduled task.</value>
IScheduledTask ScheduledTask { get; }
@@ -57,10 +57,9 @@ namespace MediaBrowser.Model.Tasks
double? CurrentProgress { get; }
/// <summary>
- /// Gets the triggers that define when the task will run.
+ /// Gets or sets the triggers that define when the task will run.
/// </summary>
/// <value>The triggers.</value>
- /// <exception cref="ArgumentNullException">value</exception>
TaskTriggerInfo[] Triggers { get; set; }
/// <summary>
diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs
index 02b29074e..a86bf2a1c 100644
--- a/MediaBrowser.Model/Tasks/ITaskManager.cs
+++ b/MediaBrowser.Model/Tasks/ITaskManager.cs
@@ -9,6 +9,10 @@ namespace MediaBrowser.Model.Tasks
{
public interface ITaskManager : IDisposable
{
+ event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+
+ event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+
/// <summary>
/// Gets the list of Scheduled Tasks.
/// </summary>
@@ -18,7 +22,7 @@ namespace MediaBrowser.Model.Tasks
/// <summary>
/// Cancels if running and queue.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam>
/// <param name="options">Task options.</param>
void CancelIfRunningAndQueue<T>(TaskOptions options)
where T : IScheduledTask;
@@ -26,21 +30,21 @@ namespace MediaBrowser.Model.Tasks
/// <summary>
/// Cancels if running and queue.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam>
void CancelIfRunningAndQueue<T>()
where T : IScheduledTask;
/// <summary>
/// Cancels if running.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam>
void CancelIfRunning<T>()
where T : IScheduledTask;
/// <summary>
/// Queues the scheduled task.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam>
/// <param name="options">Task options.</param>
void QueueScheduledTask<T>(TaskOptions options)
where T : IScheduledTask;
@@ -48,7 +52,7 @@ namespace MediaBrowser.Model.Tasks
/// <summary>
/// Queues the scheduled task.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam>
void QueueScheduledTask<T>()
where T : IScheduledTask;
@@ -58,6 +62,8 @@ namespace MediaBrowser.Model.Tasks
/// <summary>
/// Queues the scheduled task.
/// </summary>
+ /// <param name="task">The <see cref="IScheduledTask" /> to queue.</param>
+ /// <param name="options">The <see cref="TaskOptions" /> to use.</param>
void QueueScheduledTask(IScheduledTask task, TaskOptions options);
/// <summary>
@@ -67,12 +73,10 @@ namespace MediaBrowser.Model.Tasks
void AddTasks(IEnumerable<IScheduledTask> tasks);
void Cancel(IScheduledTaskWorker task);
+
Task Execute(IScheduledTaskWorker task, TaskOptions options);
void Execute<T>()
where T : IScheduledTask;
-
- event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
- event EventHandler<TaskCompletionEventArgs> TaskCompleted;
}
}
diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
index 5c30d6c22..999db9605 100644
--- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs
+++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
@@ -11,16 +11,20 @@ namespace MediaBrowser.Model.Tasks
/// <summary>
/// Fires when the trigger condition is satisfied and the task should run.
/// </summary>
- event EventHandler<EventArgs> Triggered;
+ event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the options of this task.
+ /// Gets the options of this task.
/// </summary>
- TaskOptions TaskOptions { get; set; }
+ TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
/// </summary>
+ /// <param name="lastResult">Result of the last run triggerd task.</param>
+ /// <param name="logger">The <see cref="ILogger"/>.</param>
+ /// <param name="taskName">The name of the task.</param>
+ /// <param name="isApplicationStartup">Wheter or not this is is fired during startup.</param>
void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup);
/// <summary>
diff --git a/MediaBrowser.Model/Tasks/TaskInfo.cs b/MediaBrowser.Model/Tasks/TaskInfo.cs
index 77100dfe7..16de0b121 100644
--- a/MediaBrowser.Model/Tasks/TaskInfo.cs
+++ b/MediaBrowser.Model/Tasks/TaskInfo.cs
@@ -9,6 +9,14 @@ namespace MediaBrowser.Model.Tasks
public class TaskInfo
{
/// <summary>
+ /// Initializes a new instance of the <see cref="TaskInfo"/> class.
+ /// </summary>
+ public TaskInfo()
+ {
+ Triggers = Array.Empty<TaskTriggerInfo>();
+ }
+
+ /// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
@@ -67,13 +75,5 @@ namespace MediaBrowser.Model.Tasks
/// </summary>
/// <value>The key.</value>
public string Key { get; set; }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TaskInfo"/> class.
- /// </summary>
- public TaskInfo()
- {
- Triggers = Array.Empty<TaskTriggerInfo>();
- }
}
}
diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
index 5aeaffc2b..f8a8c727e 100644
--- a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
+++ b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
@@ -10,6 +10,12 @@ namespace MediaBrowser.Model.Tasks
/// </summary>
public class TaskTriggerInfo
{
+ public const string TriggerDaily = "DailyTrigger";
+ public const string TriggerWeekly = "WeeklyTrigger";
+ public const string TriggerInterval = "IntervalTrigger";
+ public const string TriggerSystemEvent = "SystemEventTrigger";
+ public const string TriggerStartup = "StartupTrigger";
+
/// <summary>
/// Gets or sets the type.
/// </summary>
@@ -39,11 +45,5 @@ namespace MediaBrowser.Model.Tasks
/// </summary>
/// <value>The maximum runtime ticks.</value>
public long? MaxRuntimeTicks { get; set; }
-
- public const string TriggerDaily = "DailyTrigger";
- public const string TriggerWeekly = "WeeklyTrigger";
- public const string TriggerInterval = "IntervalTrigger";
- public const string TriggerSystemEvent = "SystemEventTrigger";
- public const string TriggerStartup = "StartupTrigger";
}
}
diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs
index eebe1a903..cc600de9d 100644
--- a/MediaBrowser.Model/Updates/InstallationInfo.cs
+++ b/MediaBrowser.Model/Updates/InstallationInfo.cs
@@ -1,4 +1,5 @@
#nullable disable
+
using System;
using System.Text.Json.Serialization;
@@ -45,5 +46,11 @@ namespace MediaBrowser.Model.Updates
/// </summary>
/// <value>The checksum.</value>
public string Checksum { get; set; }
+
+ /// <summary>
+ /// Gets or sets package information for the installation.
+ /// </summary>
+ /// <value>The package information.</value>
+ public PackageInfo PackageInfo { get; set; }
}
}
diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs
index 7a82685f0..aeaaa8b35 100644
--- a/MediaBrowser.Model/Updates/PackageInfo.cs
+++ b/MediaBrowser.Model/Updates/PackageInfo.cs
@@ -1,4 +1,3 @@
-#nullable enable
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -16,7 +15,6 @@ namespace MediaBrowser.Model.Updates
public PackageInfo()
{
Versions = Array.Empty<VersionInfo>();
- Id = string.Empty;
Category = string.Empty;
Name = string.Empty;
Overview = string.Empty;
@@ -65,7 +63,7 @@ namespace MediaBrowser.Model.Updates
/// </summary>
/// <value>The name.</value>
[JsonPropertyName("guid")]
- public string Id { get; set; }
+ public Guid Id { get; set; }
/// <summary>
/// Gets or sets the versions.
diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs
index 209092265..03a540dde 100644
--- a/MediaBrowser.Model/Updates/VersionInfo.cs
+++ b/MediaBrowser.Model/Updates/VersionInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System.Text.Json.Serialization;
using SysVersion = System.Version;
diff --git a/MediaBrowser.Model/Users/UserAction.cs b/MediaBrowser.Model/Users/UserAction.cs
deleted file mode 100644
index 7646db4a8..000000000
--- a/MediaBrowser.Model/Users/UserAction.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Users
-{
- public class UserAction
- {
- public string Id { get; set; }
-
- public string ServerId { get; set; }
-
- public Guid UserId { get; set; }
-
- public Guid ItemId { get; set; }
-
- public UserActionType Type { get; set; }
-
- public DateTime Date { get; set; }
-
- public long? PositionTicks { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 37da04adf..111070d81 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -10,6 +10,56 @@ namespace MediaBrowser.Model.Users
{
public class UserPolicy
{
+ public UserPolicy()
+ {
+ IsHidden = true;
+
+ EnableContentDeletion = false;
+ EnableContentDeletionFromFolders = Array.Empty<string>();
+
+ EnableSyncTranscoding = true;
+ EnableMediaConversion = true;
+
+ EnableMediaPlayback = true;
+ EnableAudioPlaybackTranscoding = true;
+ EnableVideoPlaybackTranscoding = true;
+ EnablePlaybackRemuxing = true;
+ ForceRemoteSourceTranscoding = false;
+ EnableLiveTvManagement = true;
+ EnableLiveTvAccess = true;
+
+ // Without this on by default, admins won't be able to do this
+ // Improve in the future
+ EnableLiveTvManagement = true;
+
+ EnableSharedDeviceControl = true;
+
+ BlockedTags = Array.Empty<string>();
+ BlockUnratedItems = Array.Empty<UnratedItem>();
+
+ EnableUserPreferenceAccess = true;
+
+ AccessSchedules = Array.Empty<AccessSchedule>();
+
+ LoginAttemptsBeforeLockout = -1;
+
+ MaxActiveSessions = 0;
+
+ EnableAllChannels = true;
+ EnabledChannels = Array.Empty<Guid>();
+
+ EnableAllFolders = true;
+ EnabledFolders = Array.Empty<Guid>();
+
+ EnabledDevices = Array.Empty<string>();
+ EnableAllDevices = true;
+
+ EnableContentDownloading = true;
+ EnablePublicSharing = true;
+ EnableRemoteAccess = true;
+ SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
+ }
+
/// <summary>
/// Gets or sets a value indicating whether this instance is administrator.
/// </summary>
@@ -112,55 +162,5 @@ namespace MediaBrowser.Model.Users
/// </summary>
/// <value>Access level to SyncPlay features.</value>
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
-
- public UserPolicy()
- {
- IsHidden = true;
-
- EnableContentDeletion = false;
- EnableContentDeletionFromFolders = Array.Empty<string>();
-
- EnableSyncTranscoding = true;
- EnableMediaConversion = true;
-
- EnableMediaPlayback = true;
- EnableAudioPlaybackTranscoding = true;
- EnableVideoPlaybackTranscoding = true;
- EnablePlaybackRemuxing = true;
- ForceRemoteSourceTranscoding = false;
- EnableLiveTvManagement = true;
- EnableLiveTvAccess = true;
-
- // Without this on by default, admins won't be able to do this
- // Improve in the future
- EnableLiveTvManagement = true;
-
- EnableSharedDeviceControl = true;
-
- BlockedTags = Array.Empty<string>();
- BlockUnratedItems = Array.Empty<UnratedItem>();
-
- EnableUserPreferenceAccess = true;
-
- AccessSchedules = Array.Empty<AccessSchedule>();
-
- LoginAttemptsBeforeLockout = -1;
-
- MaxActiveSessions = 0;
-
- EnableAllChannels = true;
- EnabledChannels = Array.Empty<Guid>();
-
- EnableAllFolders = true;
- EnabledFolders = Array.Empty<Guid>();
-
- EnabledDevices = Array.Empty<string>();
- EnableAllDevices = true;
-
- EnableContentDownloading = true;
- EnablePublicSharing = true;
- EnableRemoteAccess = true;
- SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
- }
}
}
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 19a42d506..fb1d4f490 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -102,10 +102,8 @@ namespace MediaBrowser.Providers.Manager
{
saveLocally = false;
- var season = item as Season;
-
// If season is virtual under a physical series, save locally if using compatible convention
- if (season != null && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible)
+ if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible)
{
var series = season.Series;
@@ -138,7 +136,7 @@ namespace MediaBrowser.Providers.Manager
var memoryStream = new MemoryStream();
await using (source.ConfigureAwait(false))
{
- await source.CopyToAsync(memoryStream).ConfigureAwait(false);
+ await source.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
}
source = memoryStream;
@@ -174,7 +172,9 @@ namespace MediaBrowser.Providers.Manager
SetImagePath(item, type, imageIndex, savedPaths[0]);
// Delete the current path
- if (currentImageIsLocalFile && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase))
+ if (currentImageIsLocalFile
+ && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase)
+ && (saveLocally || currentImagePath.Contains(_config.ApplicationPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase)))
{
var currentPath = currentImagePath;
@@ -263,7 +263,8 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
- await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index ffc6889fa..966a3d822 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Manager
/// <summary>
/// Image types that are only one per item.
/// </summary>
- private readonly ImageType[] _singularImages =
+ private static readonly ImageType[] _singularImages =
{
ImageType.Primary,
ImageType.Art,
@@ -208,9 +208,14 @@ namespace MediaBrowser.Providers.Manager
/// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns>
private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit, int screenshotLimit)
{
- if (_singularImages.Any(i => images.Contains(i) && !HasImage(item, i) && savedOptions.GetLimit(i) > 0))
+ // Using .Any causes the creation of a DisplayClass aka. variable capture
+ for (var i = 0; i < _singularImages.Length; i++)
{
- return false;
+ var type = _singularImages[i];
+ if (images.Contains(type) && !HasImage(item, type) && savedOptions.GetLimit(type) > 0)
+ {
+ return false;
+ }
}
if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit)
@@ -329,7 +334,7 @@ namespace MediaBrowser.Providers.Manager
var deleted = false;
var deletedImages = new List<ItemImageInfo>();
- foreach (var image in item.GetImages(type).ToList())
+ foreach (var image in item.GetImages(type))
{
if (!image.IsLocalFile)
{
@@ -359,9 +364,10 @@ namespace MediaBrowser.Providers.Manager
{
var changed = false;
- foreach (var type in _singularImages)
+ for (var i = 0; i < _singularImages.Length; i++)
{
- var image = images.FirstOrDefault(i => i.Type == type);
+ var type = _singularImages[i];
+ var image = GetFirstLocalImageInfoByType(images, type);
if (image != null)
{
@@ -423,15 +429,29 @@ namespace MediaBrowser.Providers.Manager
return changed;
}
+ private static LocalImageInfo GetFirstLocalImageInfoByType(IReadOnlyList<LocalImageInfo> images, ImageType type)
+ {
+ var len = images.Count;
+ for (var i = 0; i < len; i++)
+ {
+ var image = images[i];
+ if (image.Type == type)
+ {
+ return image;
+ }
+ }
+
+ return null;
+ }
+
private bool UpdateMultiImages(BaseItem item, List<LocalImageInfo> images, ImageType type)
{
var changed = false;
- var newImages = images.Where(i => i.Type == type).ToList();
-
- var newImageFileInfos = newImages
- .Select(i => i.FileInfo)
- .ToList();
+ var newImageFileInfos = images
+ .FindAll(i => i.Type == type)
+ .Select(i => i.FileInfo)
+ .ToList();
if (item.AddImages(type, newImageFileInfos))
{
@@ -469,6 +489,7 @@ namespace MediaBrowser.Providers.Manager
try
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await _providerManager.SaveImage(
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 8b3ca17ca..827cb69b9 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -28,8 +28,11 @@ namespace MediaBrowser.Providers.Manager
ProviderManager = providerManager;
FileSystem = fileSystem;
LibraryManager = libraryManager;
+ ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
}
+ protected ItemImageProvider ImageProvider { get; }
+
protected IServerConfigurationManager ServerConfigurationManager { get; }
protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; }
@@ -88,7 +91,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
var localImagesFailed = false;
var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList();
@@ -97,7 +99,7 @@ namespace MediaBrowser.Providers.Manager
try
{
// Always validate images and check for new locally stored ones.
- if (itemImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService))
+ if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService))
{
updateType |= ItemUpdateType.ImageUpdate;
}
@@ -143,7 +145,7 @@ namespace MediaBrowser.Providers.Manager
// await FindIdentities(id, cancellationToken).ConfigureAwait(false);
id.IsAutomated = refreshOptions.IsAutomated;
- var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false);
+ var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
@@ -160,7 +162,7 @@ namespace MediaBrowser.Providers.Manager
if (providers.Count > 0)
{
- var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
+ var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
@@ -201,7 +203,7 @@ namespace MediaBrowser.Providers.Manager
}
// Save to database
- await SaveItemAsync(metadataResult, libraryOptions, updateType, cancellationToken).ConfigureAwait(false);
+ await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
}
await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
@@ -211,71 +213,37 @@ namespace MediaBrowser.Providers.Manager
private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result)
{
- lookupInfo.ProviderIds = result.ProviderIds;
- lookupInfo.Name = result.Name;
- lookupInfo.Year = result.ProductionYear;
+ // Episode and Season do not support Identify, so the search results are the Series'
+ switch (lookupInfo)
+ {
+ case EpisodeInfo episodeInfo:
+ episodeInfo.SeriesProviderIds = result.ProviderIds;
+ episodeInfo.ProviderIds.Clear();
+ break;
+ case SeasonInfo seasonInfo:
+ seasonInfo.SeriesProviderIds = result.ProviderIds;
+ seasonInfo.ProviderIds.Clear();
+ break;
+ default:
+ lookupInfo.ProviderIds = result.ProviderIds;
+ lookupInfo.Name = result.Name;
+ lookupInfo.Year = result.ProductionYear;
+ break;
+ }
}
- protected async Task SaveItemAsync(MetadataResult<TItemType> result, LibraryOptions libraryOptions, ItemUpdateType reason, CancellationToken cancellationToken)
+ protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
{
if (result.Item.SupportsPeople && result.People != null)
{
var baseItem = result.Item;
- LibraryManager.UpdatePeople(baseItem, result.People);
- await SavePeopleMetadataAsync(result.People, libraryOptions, cancellationToken).ConfigureAwait(false);
+ await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false);
}
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
}
- private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
- {
- var personsToSave = new List<BaseItem>();
-
- foreach (var person in people)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
- {
- var itemUpdateType = ItemUpdateType.MetadataDownload;
- var saveEntity = false;
- var personEntity = LibraryManager.GetPerson(person.Name);
- foreach (var id in person.ProviderIds)
- {
- if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
- {
- personEntity.SetProviderId(id.Key, id.Value);
- saveEntity = true;
- }
- }
-
- if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
- {
- personEntity.SetImage(
- new ItemImageInfo
- {
- Path = person.ImageUrl,
- Type = ImageType.Primary
- },
- 0);
-
- saveEntity = true;
- itemUpdateType = ItemUpdateType.ImageUpdate;
- }
-
- if (saveEntity)
- {
- personsToSave.Add(personEntity);
- await LibraryManager.RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
- }
- }
- }
-
- LibraryManager.CreateItems(personsToSave, null, CancellationToken.None);
- }
-
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
{
item.AfterMetadataRefresh();
@@ -329,8 +297,7 @@ namespace MediaBrowser.Providers.Manager
return true;
}
- var folder = item as Folder;
- if (folder != null)
+ if (item is Folder folder)
{
return folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks;
}
@@ -384,8 +351,7 @@ namespace MediaBrowser.Providers.Manager
private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IList<BaseItem> children)
{
- var folder = item as Folder;
- if (folder != null && folder.SupportsCumulativeRunTimeTicks)
+ if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks)
{
long ticks = 0;
@@ -473,7 +439,7 @@ namespace MediaBrowser.Providers.Manager
if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) ||
(originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
{
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updateType |= ItemUpdateType.MetadataEdit;
}
return updateType;
@@ -493,7 +459,7 @@ namespace MediaBrowser.Providers.Manager
if (currentList.Length != item.Genres.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Genres.OrderBy(i => i), StringComparer.OrdinalIgnoreCase))
{
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updateType |= ItemUpdateType.MetadataEdit;
}
}
@@ -514,7 +480,7 @@ namespace MediaBrowser.Providers.Manager
if (currentList.Length != item.Studios.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Studios.OrderBy(i => i), StringComparer.OrdinalIgnoreCase))
{
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updateType |= ItemUpdateType.MetadataEdit;
}
}
@@ -529,7 +495,7 @@ namespace MediaBrowser.Providers.Manager
{
if (item.UpdateRatingToItems(children))
{
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updateType |= ItemUpdateType.MetadataEdit;
}
}
@@ -613,7 +579,7 @@ namespace MediaBrowser.Providers.Manager
protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options)
{
// Get providers to refresh
- var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)).ToList();
+ var providers = allImageProviders.Where(i => !(i is ILocalImageProvider));
var dateLastImageRefresh = item.DateLastRefreshed;
@@ -625,15 +591,13 @@ namespace MediaBrowser.Providers.Manager
providers = providers
.Where(i =>
{
- var hasFileChangeMonitor = i as IHasItemChangeMonitor;
- if (hasFileChangeMonitor != null)
+ if (i is IHasItemChangeMonitor hasFileChangeMonitor)
{
return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
}
return false;
- })
- .ToList();
+ });
}
return providers;
@@ -686,7 +650,7 @@ namespace MediaBrowser.Providers.Manager
var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
.ConfigureAwait(false);
- refreshResult.UpdateType = refreshResult.UpdateType | remoteResult.UpdateType;
+ refreshResult.UpdateType |= remoteResult.UpdateType;
refreshResult.ErrorMessage = remoteResult.ErrorMessage;
refreshResult.Failures += remoteResult.Failures;
}
@@ -706,9 +670,15 @@ namespace MediaBrowser.Providers.Manager
if (localItem.HasMetadata)
{
+ foreach (var remoteImage in localItem.RemoteImages)
+ {
+ await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false);
+ refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
+ }
+
if (imageService.MergeImages(item, localItem.Images))
{
- refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.ImageUpdate;
+ refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
}
if (localItem.UserDataList != null)
@@ -717,7 +687,7 @@ namespace MediaBrowser.Providers.Manager
}
MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true);
- refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport;
+ refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
// Only one local provider allowed per item
if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item))
@@ -749,7 +719,7 @@ namespace MediaBrowser.Providers.Manager
var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
.ConfigureAwait(false);
- refreshResult.UpdateType = refreshResult.UpdateType | remoteResult.UpdateType;
+ refreshResult.UpdateType |= remoteResult.UpdateType;
refreshResult.ErrorMessage = remoteResult.ErrorMessage;
refreshResult.Failures += remoteResult.Failures;
}
@@ -845,7 +815,7 @@ namespace MediaBrowser.Providers.Manager
MergeData(result, temp, Array.Empty<MetadataField>(), false, false);
MergeNewData(temp.Item, id);
- refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload;
+ refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
}
else
{
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 913f14d9b..2dfaa372c 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -25,7 +25,6 @@ using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using Priority_Queue;
@@ -60,8 +59,8 @@ namespace MediaBrowser.Providers.Manager
private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>();
private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>();
- private IEnumerable<IMetadataSaver> _savers;
- private IExternalId[] _externalIds;
+ private IMetadataSaver[] _savers = Array.Empty<IMetadataSaver>();
+ private IExternalId[] _externalIds = Array.Empty<IExternalId>();
private bool _isProcessingRefreshQueue;
private bool _disposed;
@@ -125,7 +124,7 @@ namespace MediaBrowser.Providers.Manager
_externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
_savers = metadataSavers
- .Where(i => !(i is IConfigurableProvider configurable) || configurable.IsEnabled)
+ .Where(i => i is not IConfigurableProvider configurable || configurable.IsEnabled)
.ToArray();
}
@@ -168,7 +167,7 @@ namespace MediaBrowser.Providers.Manager
throw new HttpRequestException("Invalid image received.", null, response.StatusCode);
}
- var contentType = response.Content.Headers.ContentType.MediaType;
+ var contentType = response.Content.Headers.ContentType?.MediaType;
// Workaround for tvheadend channel icons
// TODO: Isolate this hack into the tvh plugin
@@ -242,6 +241,7 @@ namespace MediaBrowser.Providers.Manager
languages.Add(preferredLanguage);
}
+ // TODO include [query.IncludeAllLanguages] as an argument to the providers
var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -869,14 +869,14 @@ namespace MediaBrowser.Providers.Manager
}
}
}
- catch (Exception)
+#pragma warning disable CA1031 // do not catch general exception types
+ catch (Exception ex)
+#pragma warning restore CA1031 // do not catch general exception types
{
- // Logged at lower levels
+ _logger.LogError(ex, "Provider {ProviderName} failed to retrieve search results", provider.Name);
}
}
- // _logger.LogDebug("Returning search results {0}", _json.SerializeToString(resultList));
-
return resultList;
}
@@ -1021,26 +1021,26 @@ namespace MediaBrowser.Providers.Manager
// TODO: Need to hunt down the conditions for this happening
_activeRefreshes.AddOrUpdate(
id,
- (_) => throw new Exception(
+ (_) => throw new InvalidOperationException(
string.Format(
CultureInfo.InvariantCulture,
"Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running",
item.GetType().Name,
item.Id.ToString("N", CultureInfo.InvariantCulture))),
- (_, __) => progress);
+ (_, _) => progress);
RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
}
/// <inheritdoc/>
- public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority)
+ public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
{
if (_disposed)
{
return;
}
- _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(id, options), (int)priority);
+ _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(itemId, options), (int)priority);
lock (_refreshQueueLock)
{
@@ -1073,17 +1073,16 @@ namespace MediaBrowser.Providers.Manager
try
{
var item = libraryManager.GetItemById(refreshItem.Item1);
- if (item != null)
+ if (item == null)
{
- // Try to throttle this a little bit.
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+ continue;
+ }
- var task = item is MusicArtist artist
- ? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
- : RefreshItem(item, refreshItem.Item2, cancellationToken);
+ var task = item is MusicArtist artist
+ ? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
+ : RefreshItem(item, refreshItem.Item2, cancellationToken);
- await task.ConfigureAwait(false);
- }
+ await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -1112,7 +1111,7 @@ namespace MediaBrowser.Providers.Manager
await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
break;
case Folder folder:
- await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
+ await folder.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
}
}
@@ -1123,7 +1122,7 @@ namespace MediaBrowser.Providers.Manager
{
await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
- await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
+ await child.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -1145,7 +1144,7 @@ namespace MediaBrowser.Providers.Manager
.Select(i => i.MusicArtist)
.Where(i => i != null);
- var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true));
+ var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), options, true, cancellationToken));
await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs
index 5621d2b86..e5aa64b28 100644
--- a/MediaBrowser.Providers/Manager/ProviderUtils.cs
+++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs
@@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.Manager
}
}
- if (replaceData || !target.CommunityRating.HasValue || (source.CommunityRating.HasValue && string.Equals(sourceResult.Provider, "The Open Movie Database", StringComparison.OrdinalIgnoreCase)))
+ if (replaceData || !target.CommunityRating.HasValue)
{
target.CommunityRating = source.CommunityRating;
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 071a149db..cdb07a15d 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -19,10 +19,10 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
- <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" />
- <PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
+ <PackageReference Include="TMDbLib" Version="1.8.1" />
</ItemGroup>
<PropertyGroup>
@@ -30,20 +30,17 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
+ <AnalysisMode Condition=" '$(Configuration)' == 'Debug'">AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<None Remove="Plugins\AudioDb\Configuration\config.html" />
<EmbeddedResource Include="Plugins\AudioDb\Configuration\config.html" />
diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index 64ad1bddf..03e45fb86 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -137,9 +137,7 @@ namespace MediaBrowser.Providers.MediaInfo
return false;
}
- var audio = item as Audio;
-
- return audio != null;
+ return item is Audio;
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
index 945463666..cf271e7db 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
@@ -111,6 +111,11 @@ namespace MediaBrowser.Providers.MediaInfo
audio.Name = data.Name;
}
+ if (!string.IsNullOrEmpty(data.ForcedSortName))
+ {
+ audio.ForcedSortName = data.ForcedSortName;
+ }
+
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List<PersonInfo>();
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 74849a522..12e1fbea5 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -111,10 +111,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- if (streamFileNames == null)
- {
- streamFileNames = Array.Empty<string>();
- }
+ streamFileNames ??= Array.Empty<string>();
mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
@@ -150,7 +147,8 @@ namespace MediaBrowser.Providers.MediaInfo
{
Path = path,
Protocol = protocol,
- VideoType = item.VideoType
+ VideoType = item.VideoType,
+ IsoType = item.IsoType
}
},
cancellationToken);
@@ -394,6 +392,12 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
+ if (video is MusicVideo musicVideo)
+ {
+ musicVideo.Album = data.Album;
+ musicVideo.Artists = data.Artists;
+ }
+
if (data.ProductionYear.HasValue)
{
if (!video.ProductionYear.HasValue || isFullRefresh)
@@ -436,6 +440,11 @@ namespace MediaBrowser.Providers.MediaInfo
video.Name = data.Name;
}
}
+
+ if (!string.IsNullOrWhiteSpace(data.ForcedSortName))
+ {
+ video.ForcedSortName = data.ForcedSortName;
+ }
}
// If we don't have a ProductionYear try and get it from PremiereDate
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
index 912aedb0d..44ab5aa5b 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
@@ -172,9 +172,7 @@ namespace MediaBrowser.Providers.MediaInfo
SubtitleFetcherOrder = subtitleFetcherOrder
};
- var episode = video as Episode;
-
- if (episode != null)
+ if (video is Episode episode)
{
request.IndexNumberEnd = episode.IndexNumberEnd;
request.SeriesName = episode.SeriesName;
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index e9f999c6d..3cd7ec772 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -15,17 +14,6 @@ namespace MediaBrowser.Providers.MediaInfo
{
private readonly ILocalizationManager _localization;
- private static readonly HashSet<string> SubtitleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- ".srt",
- ".ssa",
- ".ass",
- ".sub",
- ".smi",
- ".sami",
- ".vtt"
- };
-
public SubtitleResolver(ILocalizationManager localization)
{
_localization = localization;
@@ -66,138 +54,142 @@ namespace MediaBrowser.Providers.MediaInfo
return streams;
}
- public List<string> GetExternalSubtitleFiles(
+ public IEnumerable<string> GetExternalSubtitleFiles(
Video video,
IDirectoryService directoryService,
bool clearCache)
{
- var list = new List<string>();
-
if (!video.IsFileProtocol)
{
- return list;
+ yield break;
}
var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache);
foreach (var stream in streams)
{
- list.Add(stream.Path);
+ yield return stream.Path;
}
-
- return list;
- }
-
- private void AddExternalSubtitleStreams(
- List<MediaStream> streams,
- string folder,
- string videoPath,
- int startIndex,
- IDirectoryService directoryService,
- bool clearCache)
- {
- var files = directoryService.GetFilePaths(folder, clearCache).OrderBy(i => i).ToArray();
-
- AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
}
public void AddExternalSubtitleStreams(
List<MediaStream> streams,
string videoPath,
int startIndex,
- string[] files)
+ IReadOnlyList<string> files)
{
- var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(videoPath);
- videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoFileNameWithoutExtension);
+ var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath);
- foreach (var fullName in files)
+ for (var i = 0; i < files.Count; i++)
{
- var extension = Path.GetExtension(fullName);
-
- if (!SubtitleExtensions.Contains(extension))
- {
- continue;
- }
-
- var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullName);
- fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fileNameWithoutExtension);
-
- if (!string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase) &&
- !fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase))
+ var fullName = files[i];
+ var extension = Path.GetExtension(fullName.AsSpan());
+ if (!IsSubtitleExtension(extension))
{
continue;
}
- var codec = Path.GetExtension(fullName).ToLowerInvariant().TrimStart('.');
+ var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName);
- if (string.Equals(codec, "txt", StringComparison.OrdinalIgnoreCase))
- {
- codec = "srt";
- }
+ MediaStream mediaStream;
- // If the subtitle file matches the video file name
- if (string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+ // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot
+ if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
- streams.Add(new MediaStream
+ mediaStream = new MediaStream
{
Index = startIndex++,
Type = MediaStreamType.Subtitle,
IsExternal = true,
- Path = fullName,
- Codec = codec
- });
+ Path = fullName
+ };
}
- else if (fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase))
+ else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
+ && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
+ && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
- var isForced = fullName.IndexOf(".forced.", StringComparison.OrdinalIgnoreCase) != -1 ||
- fullName.IndexOf(".foreign.", StringComparison.OrdinalIgnoreCase) != -1;
+ var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase)
+ || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase);
- var isDefault = fullName.IndexOf(".default.", StringComparison.OrdinalIgnoreCase) != -1;
+ var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase);
// Support xbmc naming conventions - 300.spanish.srt
- var language = fileNameWithoutExtension
- .Replace(".forced", string.Empty, StringComparison.OrdinalIgnoreCase)
- .Replace(".foreign", string.Empty, StringComparison.OrdinalIgnoreCase)
- .Replace(".default", string.Empty, StringComparison.OrdinalIgnoreCase)
- .Split('.')
- .LastOrDefault();
+ var languageSpan = fileNameWithoutExtension;
+ while (languageSpan.Length > 0)
+ {
+ var lastDot = languageSpan.LastIndexOf('.');
+ var currentSlice = languageSpan[lastDot..];
+ if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase)
+ || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase)
+ || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase))
+ {
+ languageSpan = languageSpan[..lastDot];
+ continue;
+ }
+
+ languageSpan = languageSpan[(lastDot + 1)..];
+ break;
+ }
// Try to translate to three character code
// Be flexible and check against both the full and three character versions
+ var language = languageSpan.ToString();
var culture = _localization.FindLanguageInfo(language);
- if (culture != null)
- {
- language = culture.ThreeLetterISOLanguageName;
- }
+ language = culture == null ? language : culture.ThreeLetterISOLanguageName;
- streams.Add(new MediaStream
+ mediaStream = new MediaStream
{
Index = startIndex++,
Type = MediaStreamType.Subtitle,
IsExternal = true,
Path = fullName,
- Codec = codec,
Language = language,
IsForced = isForced,
IsDefault = isDefault
- });
+ };
+ }
+ else
+ {
+ continue;
}
+
+ mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant();
+
+ streams.Add(mediaStream);
}
}
- private string NormalizeFilenameForSubtitleComparison(string filename)
+ private static bool IsSubtitleExtension(ReadOnlySpan<char> extension)
+ {
+ return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static ReadOnlySpan<char> NormalizeFilenameForSubtitleComparison(string filename)
{
// Try to account for sloppy file naming
filename = filename.Replace("_", string.Empty, StringComparison.Ordinal);
filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal);
+ return Path.GetFileNameWithoutExtension(filename.AsSpan());
+ }
- // can't normalize this due to languages such as pt-br
- // filename = filename.Replace("-", string.Empty);
-
- // filename = filename.Replace(".", string.Empty);
+ private void AddExternalSubtitleStreams(
+ List<MediaStream> streams,
+ string folder,
+ string videoPath,
+ int startIndex,
+ IDirectoryService directoryService,
+ bool clearCache)
+ {
+ var files = directoryService.GetFilePaths(folder, clearCache, true);
- return filename;
+ AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
index c36c3af6a..30af6710a 100644
--- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
@@ -154,9 +154,7 @@ namespace MediaBrowser.Providers.MediaInfo
return false;
}
- var video = item as Video;
-
- if (video != null && !video.IsPlaceHolder && video.IsCompleteMedia)
+ if (item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia)
{
return true;
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
index cd9e47743..85a28747f 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.AudioDb
{
@@ -22,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory)
{
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
index f463a3566..25bb3f9ce 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
@@ -19,7 +19,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Music;
namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -29,7 +28,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public static AudioDbAlbumProvider Current;
@@ -171,7 +170,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
index 36700d191..db8536cc9 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.AudioDb
{
@@ -22,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public AudioDbArtistImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory)
{
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
index 7a15adb8e..cbb61fa35 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
@@ -18,7 +18,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Music;
namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -31,7 +30,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory)
{
@@ -155,7 +154,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
index ce9392402..2eab95294 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
@@ -69,58 +69,52 @@ namespace MediaBrowser.Providers.Music
private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream)
{
- using (var oReader = new StreamReader(stream, Encoding.UTF8))
+ using var oReader = new StreamReader(stream, Encoding.UTF8);
+ var settings = new XmlReaderSettings()
{
- var settings = new XmlReaderSettings()
- {
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
- };
+ ValidationType = ValidationType.None,
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true
+ };
- using (var reader = XmlReader.Create(oReader, settings))
- {
- reader.MoveToContent();
- reader.Read();
+ using var reader = XmlReader.Create(oReader, settings);
+ reader.MoveToContent();
+ reader.Read();
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
{
- if (reader.NodeType == XmlNodeType.Element)
+ case "artist-list":
{
- switch (reader.Name)
+ if (reader.IsEmptyElement)
{
- case "artist-list":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subReader = reader.ReadSubtree())
- {
- return ParseArtistList(subReader).ToList();
- }
- }
-
- default:
- {
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
+
+ using var subReader = reader.ReadSubtree();
+ return ParseArtistList(subReader).ToList();
}
- else
+
+ default:
{
- reader.Read();
+ reader.Skip();
+ break;
}
}
-
- return Enumerable.Empty<RemoteSearchResult>();
+ }
+ else
+ {
+ reader.Read();
}
}
+
+ return Enumerable.Empty<RemoteSearchResult>();
}
private IEnumerable<RemoteSearchResult> ParseArtistList(XmlReader reader)
@@ -145,13 +139,11 @@ namespace MediaBrowser.Providers.Music
var mbzId = reader.GetAttribute("id");
- using (var subReader = reader.ReadSubtree())
+ using var subReader = reader.ReadSubtree();
+ var artist = ParseArtist(subReader, mbzId);
+ if (artist != null)
{
- var artist = ParseArtist(subReader, mbzId);
- if (artist != null)
- {
- yield return artist;
- }
+ yield return artist;
}
break;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index ef7933b1a..0023d5959 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -128,53 +128,49 @@ namespace MediaBrowser.Providers.Music
private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream)
{
- using (var oReader = new StreamReader(stream, Encoding.UTF8))
+ using var oReader = new StreamReader(stream, Encoding.UTF8);
+ var settings = new XmlReaderSettings()
+ {
+ ValidationType = ValidationType.None,
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true
+ };
+
+ using var reader = XmlReader.Create(oReader, settings);
+ var results = ReleaseResult.Parse(reader);
+
+ return results.Select(i =>
{
- var settings = new XmlReaderSettings()
+ var result = new RemoteSearchResult
{
- ValidationType = ValidationType.None,
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true
+ Name = i.Title,
+ ProductionYear = i.Year
};
- using (var reader = XmlReader.Create(oReader, settings))
+ if (i.Artists.Count > 0)
{
- var results = ReleaseResult.Parse(reader);
-
- return results.Select(i =>
+ result.AlbumArtist = new RemoteSearchResult
{
- var result = new RemoteSearchResult
- {
- Name = i.Title,
- ProductionYear = i.Year
- };
-
- if (i.Artists.Count > 0)
- {
- result.AlbumArtist = new RemoteSearchResult
- {
- SearchProviderName = Name,
- Name = i.Artists[0].Item1
- };
+ SearchProviderName = Name,
+ Name = i.Artists[0].Item1
+ };
- result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2);
- }
-
- if (!string.IsNullOrWhiteSpace(i.ReleaseId))
- {
- result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId);
- }
+ result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2);
+ }
- if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId))
- {
- result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId);
- }
+ if (!string.IsNullOrWhiteSpace(i.ReleaseId))
+ {
+ result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId);
+ }
- return result;
- });
+ if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId))
+ {
+ result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId);
}
- }
+
+ return result;
+ });
}
/// <inheritdoc />
@@ -339,10 +335,8 @@ namespace MediaBrowser.Providers.Music
continue;
}
- using (var subReader = reader.ReadSubtree())
- {
- return ParseReleaseList(subReader).ToList();
- }
+ using var subReader = reader.ReadSubtree();
+ return ParseReleaseList(subReader).ToList();
}
default:
@@ -383,13 +377,11 @@ namespace MediaBrowser.Providers.Music
var releaseId = reader.GetAttribute("id");
- using (var subReader = reader.ReadSubtree())
+ using var subReader = reader.ReadSubtree();
+ var release = ParseRelease(subReader, releaseId);
+ if (release != null)
{
- var release = ParseRelease(subReader, releaseId);
- if (release != null)
- {
- yield return release;
- }
+ yield return release;
}
break;
@@ -460,14 +452,12 @@ namespace MediaBrowser.Providers.Music
case "artist-credit":
{
- using (var subReader = reader.ReadSubtree())
- {
- var artist = ParseArtistCredit(subReader);
+ using var subReader = reader.ReadSubtree();
+ var artist = ParseArtistCredit(subReader);
- if (!string.IsNullOrEmpty(artist.Item1))
- {
- result.Artists.Add(artist);
- }
+ if (!string.IsNullOrEmpty(artist.Item1))
+ {
+ result.Artists.Add(artist);
}
break;
@@ -505,12 +495,10 @@ namespace MediaBrowser.Providers.Music
switch (reader.Name)
{
case "name-credit":
- {
- using (var subReader = reader.ReadSubtree())
- {
- return ParseArtistNameCredit(subReader);
- }
- }
+ {
+ using var subReader = reader.ReadSubtree();
+ return ParseArtistNameCredit(subReader);
+ }
default:
{
@@ -545,10 +533,8 @@ namespace MediaBrowser.Providers.Music
case "artist":
{
var id = reader.GetAttribute("id");
- using (var subReader = reader.ReadSubtree())
- {
- return ParseArtistArtistCredit(subReader, id);
- }
+ using var subReader = reader.ReadSubtree();
+ return ParseArtistArtistCredit(subReader, id);
}
default:
@@ -647,47 +633,43 @@ namespace MediaBrowser.Providers.Music
IgnoreComments = true
};
- using (var reader = XmlReader.Create(oReader, settings))
- {
- reader.MoveToContent();
- reader.Read();
+ using var reader = XmlReader.Create(oReader, settings);
+ reader.MoveToContent();
+ reader.Read();
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
{
- if (reader.NodeType == XmlNodeType.Element)
+ switch (reader.Name)
{
- switch (reader.Name)
+ case "release-group-list":
{
- case "release-group-list":
+ if (reader.IsEmptyElement)
{
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subReader = reader.ReadSubtree())
- {
- return GetFirstReleaseGroupId(subReader);
- }
+ reader.Read();
+ continue;
}
- default:
- {
- reader.Skip();
- break;
- }
+ using var subReader = reader.ReadSubtree();
+ return GetFirstReleaseGroupId(subReader);
+ }
+
+ default:
+ {
+ reader.Skip();
+ break;
}
- }
- else
- {
- reader.Read();
}
}
-
- return null;
+ else
+ {
+ reader.Read();
+ }
}
+
+ return null;
}
private string GetFirstReleaseGroupId(XmlReader reader)
@@ -756,7 +738,10 @@ namespace MediaBrowser.Providers.Music
_stopWatchMusicBrainz.Restart();
using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
- response = await _httpClientFactory.CreateClient(NamedClient.MusicBrainz).SendAsync(request).ConfigureAwait(false);
+ response = await _httpClientFactory
+ .CreateClient(NamedClient.MusicBrainz)
+ .SendAsync(request, cancellationToken)
+ .ConfigureAwait(false);
// We retry a finite number of times, and only whilst MB is indicating 503 (throttling).
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 97fcbfb6f..428b0ded1 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
_configurationManager = configurationManager;
_appHost = appHost;
- _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions());
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
_jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter());
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index e3301ff32..46d303890 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -6,9 +6,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Text;
using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
@@ -39,7 +37,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
_configurationManager = configurationManager;
_appHost = appHost;
- _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions());
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
_jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter());
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index df1e12240..5ad61c567 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -58,7 +58,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
var language = item.GetPreferredMetadataLanguage();
- var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
+ // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
+ var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, null, null, cancellationToken).ConfigureAwait(false);
if (collection?.Images == null)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index fcd8e614c..ca1af6c49 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -19,11 +20,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly TmdbClientManager _tmdbClientManager;
+ private readonly ILibraryManager _libraryManager;
- public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
+ public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager, ILibraryManager libraryManager)
{
_httpClientFactory = httpClientFactory;
_tmdbClientManager = tmdbClientManager;
+ _libraryManager = libraryManager;
}
public string Name => TmdbUtils.ProviderName;
@@ -83,7 +86,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
// We don't already have an Id, need to fetch it
if (tmdbId <= 0)
{
- var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false);
+ // ParseName is required here.
+ // Caller provides the filename with extension stripped and NOT the parsed filename
+ var parsedName = _libraryManager.ParseName(id.Name);
+ var cleanedName = TmdbUtils.CleanName(parsedName.Name);
+ var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, cancellationToken).ConfigureAwait(false);
if (searchResults != null && searchResults.Count > 0)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index dac9e961c..f34d689c1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -73,8 +73,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return Enumerable.Empty<RemoteImageInfo>();
}
+ // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var movie = await _tmdbClientManager
- .GetMovieAsync(movieTmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .GetMovieAsync(movieTmdbId, null, null, cancellationToken)
.ConfigureAwait(false);
if (movie?.Images == null)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index bcf9459ef..4a0884c07 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -14,6 +14,8 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
+using TMDbLib.Objects.Find;
+using TMDbLib.Objects.Search;
namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
@@ -43,63 +45,89 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
{
- var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
-
- if (tmdbId == 0)
+ if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var id))
{
- var movieResults = await _tmdbClientManager
- .SearchMovieAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
+ var movie = await _tmdbClientManager
+ .GetMovieAsync(
+ int.Parse(id, CultureInfo.InvariantCulture),
+ searchInfo.MetadataLanguage,
+ TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
+ cancellationToken)
.ConfigureAwait(false);
- var remoteSearchResults = new List<RemoteSearchResult>();
- for (var i = 0; i < movieResults.Count; i++)
+
+ var remoteResult = new RemoteSearchResult
{
- var movieResult = movieResults[i];
- var remoteSearchResult = new RemoteSearchResult
- {
- Name = movieResult.Title ?? movieResult.OriginalTitle,
- ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath),
- Overview = movieResult.Overview,
- SearchProviderName = Name
- };
+ Name = movie.Title ?? movie.OriginalTitle,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
+ Overview = movie.Overview
+ };
+
+ if (movie.ReleaseDate != null)
+ {
+ var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
+ remoteResult.PremiereDate = releaseDate;
+ remoteResult.ProductionYear = releaseDate.Year;
+ }
- var releaseDate = movieResult.ReleaseDate?.ToUniversalTime();
- remoteSearchResult.PremiereDate = releaseDate;
- remoteSearchResult.ProductionYear = releaseDate?.Year;
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
- remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCulture));
- remoteSearchResults.Add(remoteSearchResult);
+ if (!string.IsNullOrWhiteSpace(movie.ImdbId))
+ {
+ remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
}
- return remoteSearchResults;
+ return new[] { remoteResult };
}
- var movie = await _tmdbClientManager
- .GetMovieAsync(tmdbId, searchInfo.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), cancellationToken)
- .ConfigureAwait(false);
-
- var remoteResult = new RemoteSearchResult
+ IReadOnlyList<SearchMovie> movieResults;
+ if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out id))
{
- Name = movie.Title ?? movie.OriginalTitle,
- SearchProviderName = Name,
- ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
- Overview = movie.Overview
- };
-
- if (movie.ReleaseDate != null)
+ var result = await _tmdbClientManager.FindByExternalIdAsync(
+ id,
+ FindExternalSource.Imdb,
+ TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
+ cancellationToken).ConfigureAwait(false);
+ movieResults = result.MovieResults;
+ }
+ else if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out id))
{
- var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
- remoteResult.PremiereDate = releaseDate;
- remoteResult.ProductionYear = releaseDate.Year;
+ var result = await _tmdbClientManager.FindByExternalIdAsync(
+ id,
+ FindExternalSource.TvDb,
+ TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
+ cancellationToken).ConfigureAwait(false);
+ movieResults = result.MovieResults;
+ }
+ else
+ {
+ movieResults = await _tmdbClientManager
+ .SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
}
- remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
-
- if (!string.IsNullOrWhiteSpace(movie.ImdbId))
+ var len = movieResults.Count;
+ var remoteSearchResults = new RemoteSearchResult[len];
+ for (var i = 0; i < len; i++)
{
- remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
+ var movieResult = movieResults[i];
+ var remoteSearchResult = new RemoteSearchResult
+ {
+ Name = movieResult.Title ?? movieResult.OriginalTitle,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath),
+ Overview = movieResult.Overview,
+ SearchProviderName = Name
+ };
+
+ var releaseDate = movieResult.ReleaseDate?.ToUniversalTime();
+ remoteSearchResult.PremiereDate = releaseDate;
+ remoteSearchResult.ProductionYear = releaseDate?.Year;
+
+ remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCulture));
+ remoteSearchResults[i] = remoteSearchResult;
}
- return new[] { remoteResult };
+ return remoteSearchResults;
}
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
@@ -112,7 +140,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
// ParseName is required here.
// Caller provides the filename with extension stripped and NOT the parsed filename
var parsedName = _libraryManager.ParseName(info.Name);
- var searchResults = await _tmdbClientManager.SearchMovieAsync(parsedName.Name, parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ var cleanedName = TmdbUtils.CleanName(parsedName.Name);
+ var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResults.Count > 0)
{
@@ -120,6 +149,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
}
}
+ if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
+ {
+ var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ if (movieResultFromImdbId?.MovieResults.Count > 0)
+ {
+ tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString();
+ }
+ }
+
if (string.IsNullOrEmpty(tmdbId))
{
return new MetadataResult<Movie>();
@@ -137,6 +175,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var movie = new Movie
{
Name = movieResult.Title ?? movieResult.OriginalTitle,
+ OriginalTitle = movieResult.OriginalTitle,
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
Tagline = movieResult.Tagline,
ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
@@ -167,12 +206,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (ourRelease != null)
{
- var ratingPrefix = string.Equals(info.MetadataCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? string.Empty : info.MetadataCountryCode + "-";
- var newRating = ratingPrefix + ourRelease.Certification;
-
- newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase);
-
- movie.OfficialRating = newRating;
+ movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification);
}
else if (usRelease != null)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index 3f57c4bc4..e4c908a62 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -1,6 +1,5 @@
#pragma warning disable CS1591
-using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -49,37 +48,36 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var person = (Person)item;
- var personTmdbId = Convert.ToInt32(person.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (personTmdbId > 0)
+ if (!person.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId))
{
- var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
- if (personResult?.Images?.Profiles == null)
- {
- return Enumerable.Empty<RemoteImageInfo>();
- }
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- var remoteImages = new List<RemoteImageInfo>();
- var language = item.GetPreferredMetadataLanguage();
+ var language = item.GetPreferredMetadataLanguage();
+ var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), language, cancellationToken).ConfigureAwait(false);
+ if (personResult?.Images?.Profiles == null)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- for (var i = 0; i < personResult.Images.Profiles.Count; i++)
- {
- var image = personResult.Images.Profiles[i];
- remoteImages.Add(new RemoteImageInfo
- {
- ProviderName = Name,
- Type = ImageType.Primary,
- Width = image.Width,
- Height = image.Height,
- Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
- Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
- });
- }
+ var remoteImages = new RemoteImageInfo[personResult.Images.Profiles.Count];
- return remoteImages.OrderByLanguageDescending(language);
+ for (var i = 0; i < personResult.Images.Profiles.Count; i++)
+ {
+ var image = personResult.Images.Profiles[i];
+ remoteImages[i] = new RemoteImageInfo
+ {
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ Width = image.Width,
+ Height = image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
+ Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
+ };
}
- return Enumerable.Empty<RemoteImageInfo>();
+ return remoteImages.OrderByLanguageDescending(language);
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 4384c203e..6db550b1d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -30,11 +29,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
{
- var personTmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
-
- if (personTmdbId <= 0)
+ if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId))
{
- var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
+ var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (personResult != null)
{
@@ -51,19 +48,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
}
result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
- result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
+ if (!string.IsNullOrEmpty(personResult.ExternalIds.ImdbId))
+ {
+ result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
+ }
return new[] { result };
}
}
- // TODO why? Because of the old rate limit?
- if (searchInfo.IsAutomated)
- {
- // Don't hammer moviedb searching by name
- return Enumerable.Empty<RemoteSearchResult>();
- }
-
var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
var remoteSearchResults = new List<RemoteSearchResult>();
@@ -102,7 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId > 0)
{
- var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
+ var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false);
result.HasMetadata = true;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index 3b7a0b254..ba18c542f 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -63,8 +63,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var language = item.GetPreferredMetadataLanguage();
+ // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var episodeResult = await _tmdbClientManager
- .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
.ConfigureAwait(false);
var stills = episodeResult?.Images?.Stills;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 93998a110..8ec8f6464 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
var episodeResult = await _tmdbClientManager
- .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.ConfigureAwait(false);
if (episodeResult == null)
@@ -111,24 +111,31 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var item = new Episode
{
- Name = info.Name,
IndexNumber = info.IndexNumber,
ParentIndexNumber = info.ParentIndexNumber,
- IndexNumberEnd = info.IndexNumberEnd
+ IndexNumberEnd = info.IndexNumberEnd,
+ Name = episodeResult.Name,
+ PremiereDate = episodeResult.AirDate,
+ ProductionYear = episodeResult.AirDate?.Year,
+ Overview = episodeResult.Overview,
+ CommunityRating = Convert.ToSingle(episodeResult.VoteAverage)
};
- if (!string.IsNullOrEmpty(episodeResult.ExternalIds?.TvdbId))
+ var externalIds = episodeResult.ExternalIds;
+ if (!string.IsNullOrEmpty(externalIds?.TvdbId))
{
- item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId);
+ item.SetProviderId(MetadataProvider.Tvdb, externalIds.TvdbId);
}
- item.PremiereDate = episodeResult.AirDate;
- item.ProductionYear = episodeResult.AirDate?.Year;
-
- item.Name = episodeResult.Name;
- item.Overview = episodeResult.Overview;
+ if (!string.IsNullOrEmpty(externalIds?.ImdbId))
+ {
+ item.SetProviderId(MetadataProvider.Imdb, externalIds.ImdbId);
+ }
- item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage);
+ if (!string.IsNullOrEmpty(externalIds?.TvrageId))
+ {
+ item.SetProviderId(MetadataProvider.TvRage, externalIds.TvrageId);
+ }
if (episodeResult.Videos?.Results != null)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index f4ed480ae..0d23c7872 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -52,8 +52,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var language = item.GetPreferredMetadataLanguage();
+ // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var seasonResult = await _tmdbClientManager
- .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, cancellationToken)
.ConfigureAwait(false);
var posters = seasonResult?.Images?.Posters;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index d0c6b8b88..326c116b3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -54,13 +54,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId))
{
- return null;
+ return Enumerable.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
+ // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var series = await _tmdbClientManager
- .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), null, null, cancellationToken)
.ConfigureAwait(false);
if (series?.Images == null)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 942c85b90..da76345b5 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -22,15 +23,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILibraryManager _libraryManager;
private readonly TmdbClientManager _tmdbClientManager;
public TmdbSeriesProvider(
+ ILibraryManager libraryManager,
IHttpClientFactory httpClientFactory,
TmdbClientManager tmdbClientManager)
{
+ _libraryManager = libraryManager;
_httpClientFactory = httpClientFactory;
_tmdbClientManager = tmdbClientManager;
- Current = this;
}
public string Name => TmdbUtils.ProviderName;
@@ -38,13 +41,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// After TheTVDB
public int Order => 1;
- internal static TmdbSeriesProvider Current { get; private set; }
-
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
- var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
-
- if (!string.IsNullOrEmpty(tmdbId))
+ if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId))
{
var series = await _tmdbClientManager
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken)
@@ -58,9 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
- var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb);
-
- if (!string.IsNullOrEmpty(imdbId))
+ if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
{
var findResult = await _tmdbClientManager
.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken)
@@ -81,9 +78,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
- var tvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb);
-
- if (!string.IsNullOrEmpty(tvdbId))
+ if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
{
var findResult = await _tmdbClientManager
.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken)
@@ -104,7 +99,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
- var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
+ var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken: cancellationToken)
.ConfigureAwait(false);
var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
@@ -170,40 +165,32 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
- if (string.IsNullOrEmpty(tmdbId))
+ if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
{
- var imdbId = info.GetProviderId(MetadataProvider.Imdb);
-
- if (!string.IsNullOrEmpty(imdbId))
+ var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ if (searchResult?.TvResults.Count > 0)
{
- var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- if (searchResult != null)
- {
- tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
- }
+ tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
- if (string.IsNullOrEmpty(tmdbId))
+ if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
{
- var tvdbId = info.GetProviderId(MetadataProvider.Tvdb);
-
- if (!string.IsNullOrEmpty(tvdbId))
+ var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ if (searchResult?.TvResults.Count > 0)
{
- var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- if (searchResult != null)
- {
- tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
- }
+ tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
if (string.IsNullOrEmpty(tmdbId))
{
result.QueriedById = false;
- var searchResults = await _tmdbClientManager.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ // ParseName is required here.
+ // Caller provides the filename with extension stripped and NOT the parsed filename
+ var parsedName = _libraryManager.ParseName(info.Name);
+ var cleanedName = TmdbUtils.CleanName(parsedName.Name);
+ var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
if (searchResults.Count > 0)
{
@@ -211,32 +198,34 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
- if (!string.IsNullOrEmpty(tmdbId))
+ if (string.IsNullOrEmpty(tmdbId))
{
- cancellationToken.ThrowIfCancellationRequested();
+ return result;
+ }
- var tvShow = await _tmdbClientManager
- .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
- .ConfigureAwait(false);
+ cancellationToken.ThrowIfCancellationRequested();
- result = new MetadataResult<Series>
- {
- Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
- ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
- };
+ var tvShow = await _tmdbClientManager
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- foreach (var person in GetPersons(tvShow))
- {
- result.AddPerson(person);
- }
+ result = new MetadataResult<Series>
+ {
+ Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
+ ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
+ };
- result.HasMetadata = result.Item != null;
+ foreach (var person in GetPersons(tvShow))
+ {
+ result.AddPerson(person);
}
+ result.HasMetadata = result.Item != null;
+
return result;
}
- private Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
+ private static Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
{
var series = new Series
{
@@ -311,7 +300,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (ourRelease != null)
{
- series.OfficialRating = ourRelease.Rating;
+ series.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Rating);
}
else if (usRelease != null)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 2dc5cd55d..79ec6139d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -125,7 +125,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
tmdbId,
language: TmdbUtils.NormalizeLanguage(language),
includeImageLanguage: imageLanguages,
- extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings,
+ extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (series != null)
@@ -137,6 +137,56 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
/// <summary>
+ /// Gets a tv show episode group from the TMDb API based on the show id and the display order.
+ /// </summary>
+ /// <param name="tvShowId">The tv show's TMDb id.</param>
+ /// <param name="displayOrder">The display order.</param>
+ /// <param name="language">The tv show's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv show episode group information or null if not found.</returns>
+ private async Task<TvGroupCollection> GetSeriesGroupAsync(int tvShowId, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ TvGroupType? groupType =
+ string.Equals(displayOrder, "absolute", StringComparison.Ordinal) ? TvGroupType.Absolute :
+ string.Equals(displayOrder, "dvd", StringComparison.Ordinal) ? TvGroupType.DVD :
+ null;
+
+ if (groupType == null)
+ {
+ return null;
+ }
+
+ var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
+ if (_memoryCache.TryGetValue(key, out TvGroupCollection group))
+ {
+ return group;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var series = await GetSeriesAsync(tvShowId, language, imageLanguages, cancellationToken).ConfigureAwait(false);
+ var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id;
+
+ if (episodeGroupId == null)
+ {
+ return null;
+ }
+
+ group = await _tmDbClient.GetTvEpisodeGroupsAsync(
+ episodeGroupId,
+ language: TmdbUtils.NormalizeLanguage(language),
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (group != null)
+ {
+ _memoryCache.Set(key, group, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return group;
+ }
+
+ /// <summary>
/// Gets a tv season from the TMDb API based on the tv show's TMDb id.
/// </summary>
/// <param name="tvShowId">The tv season's TMDb id.</param>
@@ -177,13 +227,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="tvShowId">The tv show's TMDb id.</param>
/// <param name="seasonNumber">The season number.</param>
/// <param name="episodeNumber">The episode number.</param>
+ /// <param name="displayOrder">The display order.</param>
/// <param name="language">The episode's language.</param>
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv episode information or null if not found.</returns>
- public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string language, string imageLanguages, CancellationToken cancellationToken)
+ public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken)
{
- var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
+ var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
if (_memoryCache.TryGetValue(key, out TvEpisode episode))
{
return episode;
@@ -191,6 +242,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
+ var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken);
+ if (group != null)
+ {
+ var season = group.Groups.Find(s => s.Order == seasonNumber);
+ // Episode order starts at 0
+ var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1);
+ if (ep != null)
+ {
+ seasonNumber = ep.SeasonNumber;
+ episodeNumber = ep.EpisodeNumber;
+ }
+ }
+
episode = await _tmDbClient.GetTvEpisodeAsync(
tvShowId,
seasonNumber,
@@ -212,11 +276,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id.
/// </summary>
/// <param name="personTmdbId">The person's TMDb id.</param>
+ /// <param name="language">The episode's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb person information or null if not found.</returns>
- public async Task<Person> GetPersonAsync(int personTmdbId, CancellationToken cancellationToken)
+ public async Task<Person> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken)
{
- var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}";
+ var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out Person person))
{
return person;
@@ -226,6 +291,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
person = await _tmDbClient.GetPersonAsync(
personTmdbId,
+ TmdbUtils.NormalizeLanguage(language),
PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
cancellationToken).ConfigureAwait(false);
@@ -278,9 +344,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="name">The name of the tv show.</param>
/// <param name="language">The tv show's language.</param>
+ /// <param name="year">The year the tv show first aired.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show information.</returns>
- public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default)
{
var key = $"searchseries-{name}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series))
@@ -291,7 +358,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
+ .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index 0e8a5baab..b713736a0 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Text.RegularExpressions;
using MediaBrowser.Model.Entities;
using TMDbLib.Objects.General;
@@ -12,6 +13,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
public static class TmdbUtils
{
+ private static readonly Regex _nonWords = new (@"[\W_]+", RegexOptions.Compiled);
+
/// <summary>
/// URL of the TMDB instance to use.
/// </summary>
@@ -43,25 +46,36 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
};
/// <summary>
+ /// Cleans the name according to TMDb requirements.
+ /// </summary>
+ /// <param name="name">The name of the entity.</param>
+ /// <returns>The cleaned name.</returns>
+ public static string CleanName(string name)
+ {
+ // TMDb expects a space separated list of words make sure that is the case
+ return _nonWords.Replace(name, " ");
+ }
+
+ /// <summary>
/// Maps the TMDB provided roles for crew members to Jellyfin roles.
/// </summary>
/// <param name="crew">Crew member to map against the Jellyfin person types.</param>
/// <returns>The Jellyfin person type.</returns>
public static string MapCrewToPersonType(Crew crew)
{
- if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
- && crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase))
+ if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
+ && crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase))
{
return PersonType.Director;
}
- if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
- && crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase))
+ if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
+ && crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase))
{
return PersonType.Producer;
}
- if (crew.Department.Equals("writing", StringComparison.InvariantCultureIgnoreCase))
+ if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase))
{
return PersonType.Writer;
}
@@ -134,6 +148,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
if (parts.Length == 2)
{
+ // TMDB doesn't support Switzerland (de-CH, it-CH or fr-CH) so use the language (de, it or fr) without country code
+ if (string.Equals(parts[1], "CH", StringComparison.OrdinalIgnoreCase))
+ {
+ return parts[0];
+ }
+
language = parts[0] + "-" + parts[1].ToUpperInvariant();
}
@@ -159,5 +179,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
return imageLanguage;
}
+
+ /// <summary>
+ /// Combines the metadata country code and the parental rating from the Api into the value we store in our database.
+ /// </summary>
+ /// <param name="countryCode">The Iso 3166-1 country code of the rating country.</param>
+ /// <param name="ratingValue">The rating value returned by the Tmdb Api.</param>
+ /// <returns>The combined parental rating of country code+rating value.</returns>
+ public static string BuildParentalRating(string countryCode, string ratingValue)
+ {
+ // exclude US because we store us values as TV-14 without the country code.
+ var ratingPrefix = string.Equals(countryCode, "US", StringComparison.OrdinalIgnoreCase) ? string.Empty : countryCode + "-";
+ var newRating = ratingPrefix + ratingValue;
+
+ return newRating.Replace("DE-", "FSK-", StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index 90e13f12f..f6153dd53 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -144,7 +145,7 @@ namespace MediaBrowser.Providers.Studios
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
Directory.CreateDirectory(Path.GetDirectoryName(file));
- await using var response = await httpClient.GetStreamAsync(url).ConfigureAwait(false);
+ await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
await using var fileStream = new FileStream(file, FileMode.Create);
await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
@@ -171,25 +172,19 @@ namespace MediaBrowser.Providers.Studios
public IEnumerable<string> GetAvailableImages(string file)
{
- using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using var reader = new StreamReader(fileStream);
+ var lines = new List<string>();
+
+ foreach (var line in reader.ReadAllLines())
{
- using (var reader = new StreamReader(fileStream))
+ if (!string.IsNullOrWhiteSpace(line))
{
- var lines = new List<string>();
-
- while (!reader.EndOfStream)
- {
- var text = reader.ReadLine();
-
- if (!string.IsNullOrWhiteSpace(text))
- {
- lines.Add(text);
- }
- }
-
- return lines;
+ lines.Add(line);
}
}
+
+ return lines;
}
}
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 47e9d5ee8..6aacaa15d 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -187,36 +187,52 @@ namespace MediaBrowser.Providers.Subtitles
{
var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
- using (var stream = response.Stream)
- using (var memoryStream = new MemoryStream())
- {
- await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
- memoryStream.Position = 0;
+ using var stream = response.Stream;
+ using var memoryStream = new MemoryStream();
+ await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+ memoryStream.Position = 0;
- var savePaths = new List<string>();
- var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
+ var savePaths = new List<string>();
+ var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
- if (response.IsForced)
- {
- saveFileName += ".forced";
- }
+ if (response.IsForced)
+ {
+ saveFileName += ".forced";
+ }
- saveFileName += "." + response.Format.ToLowerInvariant();
+ saveFileName += "." + response.Format.ToLowerInvariant();
- if (saveInMediaFolder)
+ if (saveInMediaFolder)
+ {
+ var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName));
+ // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path.");
+ if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal))
{
- savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
+ savePaths.Add(mediaFolderPath);
}
+ }
- savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
+ var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
+
+ // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path.");
+ if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
+ {
+ savePaths.Add(internalPath);
+ }
+ if (savePaths.Count > 0)
+ {
await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
}
+ else
+ {
+ _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid.");
+ }
}
private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
{
- Exception exceptionToThrow = null;
+ List<Exception> exs = null;
foreach (var savePath in savePaths)
{
@@ -228,19 +244,15 @@ namespace MediaBrowser.Providers.Subtitles
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
- using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, FileStreamBufferSize, true))
- {
- await stream.CopyToAsync(fs).ConfigureAwait(false);
- }
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+ using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true);
+ await stream.CopyToAsync(fs).ConfigureAwait(false);
return;
}
catch (Exception ex)
{
- if (exceptionToThrow == null)
- {
- exceptionToThrow = ex;
- }
+ (exs ??= new List<Exception>()).Add(ex);
}
finally
{
@@ -250,9 +262,9 @@ namespace MediaBrowser.Providers.Subtitles
stream.Position = 0;
}
- if (exceptionToThrow != null)
+ if (exs != null)
{
- throw exceptionToThrow;
+ throw new AggregateException(exs);
}
}
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 40f06c731..2904b40ec 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -20,18 +20,15 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 67700f5fb..302c93f0b 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -6,12 +6,14 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Providers;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.XbmcMetadata.Configuration;
@@ -24,20 +26,35 @@ namespace MediaBrowser.XbmcMetadata.Parsers
where T : BaseItem
{
private readonly IConfigurationManager _config;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
private Dictionary<string, string> _validProviderIds;
/// <summary>
/// Initializes a new instance of the <see cref="BaseNfoParser{T}" /> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
- public BaseNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ public BaseNfoParser(
+ ILogger logger,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
{
Logger = logger;
_config = config;
ProviderManager = providerManager;
_validProviderIds = new Dictionary<string, string>();
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
}
protected CultureInfo UsCulture { get; } = new CultureInfo("en-US");
@@ -51,8 +68,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
protected virtual bool SupportsUrlAfterClosingXmlTag => false;
- protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/";
-
/// <summary>
/// Fetches metadata for an item from one xml file.
/// </summary>
@@ -169,8 +184,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
else
{
- // If the file is just an Imdb url, handle that
-
+ // If the file is just provider urls, handle that
ParseProviderLinks(item.Item, xml);
return;
@@ -209,50 +223,29 @@ namespace MediaBrowser.XbmcMetadata.Parsers
protected void ParseProviderLinks(T item, string xml)
{
- // Look for a match for the Regex pattern "tt" followed by 7 or 8 digits
- var m = Regex.Match(xml, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
- if (m.Success)
+ if (ProviderIdParsers.TryFindImdbId(xml, out var imdbId))
{
- item.SetProviderId(MetadataProvider.Imdb, m.Value);
+ item.SetProviderId(MetadataProvider.Imdb, imdbId.ToString());
}
- // Support Tmdb
- // https://www.themoviedb.org/movie/30287-fallo
- var srch = MovieDbParserSearchString;
- var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
-
- if (index != -1)
+ if (item is Movie)
{
- var tmdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
- index = tmdbId.IndexOf('-');
- if (index != -1)
+ if (ProviderIdParsers.TryFindTmdbMovieId(xml, out var tmdbId))
{
- tmdbId = tmdbId.Slice(0, index);
- }
-
- if (!tmdbId.IsEmpty
- && !tmdbId.IsWhiteSpace()
- && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture));
+ item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString());
}
}
if (item is Series)
{
- srch = "thetvdb.com/?tab=series&id=";
-
- index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+ if (ProviderIdParsers.TryFindTmdbSeriesId(xml, out var tmdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString());
+ }
- if (index != -1)
+ if (ProviderIdParsers.TryFindTvdbId(xml, out var tvdbId))
{
- var tvdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
- if (!tvdbId.IsEmpty
- && !tvdbId.IsWhiteSpace()
- && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture));
- }
+ item.SetProviderId(MetadataProvider.Tvdb, tvdbId.ToString());
}
}
}
@@ -261,6 +254,14 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var item = itemResult.Item;
+ var nfoConfiguration = _config.GetNfoConfiguration();
+ UserItemData? userData = null;
+ if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
+ {
+ var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
+ userData = _userDataManager.GetUserData(user, item);
+ }
+
switch (reader.Name)
{
// DateCreated
@@ -355,6 +356,50 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "watched":
+ {
+ var val = reader.ReadElementContentAsBoolean();
+
+ if (userData != null)
+ {
+ userData.Played = val;
+ }
+
+ break;
+ }
+
+ case "playcount":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(val) && userData != null)
+ {
+ if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var count))
+ {
+ userData.PlayCount = count;
+ }
+ }
+
+ break;
+ }
+
+ case "lastplayed":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(val) && userData != null)
+ {
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var added))
+ {
+ userData.LastPlayedDate = added.ToUniversalTime();
+ }
+ else
+ {
+ Logger.LogWarning("Invalid lastplayed value found: {Value}", val);
+ }
+ }
+
+ break;
+ }
+
case "countrycode":
{
var val = reader.ReadElementContentAsString();
@@ -617,12 +662,27 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "ratings":
+ {
+ if (!reader.IsEmptyElement)
+ {
+ using var subtree = reader.ReadSubtree();
+ FetchFromRatingsNode(subtree, item);
+ }
+ else
+ {
+ reader.Read();
+ }
+
+ break;
+ }
+
case "aired":
case "formed":
case "premiered":
case "releasedate":
{
- var formatString = _config.GetNfoConfiguration().ReleaseDateFormat;
+ var formatString = nfoConfiguration.ReleaseDateFormat;
var val = reader.ReadElementContentAsString();
@@ -640,7 +700,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "enddate":
{
- var formatString = _config.GetNfoConfiguration().ReleaseDateFormat;
+ var formatString = nfoConfiguration.ReleaseDateFormat;
var val = reader.ReadElementContentAsString();
@@ -721,6 +781,64 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "thumb":
+ {
+ var artType = reader.GetAttribute("aspect");
+ var val = reader.ReadElementContentAsString();
+
+ // skip:
+ // - empty aspect tag
+ // - empty uri
+ // - tag containing '.' because we can't set images for seasons, episodes or movie sets within series or movies
+ if (string.IsNullOrEmpty(artType) || string.IsNullOrEmpty(val) || artType.Contains('.', StringComparison.Ordinal))
+ {
+ break;
+ }
+
+ ImageType imageType = GetImageType(artType);
+
+ if (!Uri.TryCreate(val, UriKind.Absolute, out var uri))
+ {
+ Logger.LogError("Image location {Path} specified in nfo file for {ItemName} is not a valid URL or file path.", val, item.Name);
+ break;
+ }
+
+ if (uri.IsFile)
+ {
+ // only allow one item of each type
+ if (itemResult.Images.Any(x => x.Type == imageType))
+ {
+ break;
+ }
+
+ var fileSystemMetadata = _directoryService.GetFile(val);
+ // non existing file returns null
+ if (fileSystemMetadata == null || !fileSystemMetadata.Exists)
+ {
+ Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, item.Name);
+ break;
+ }
+
+ itemResult.Images.Add(new LocalImageInfo()
+ {
+ FileInfo = fileSystemMetadata,
+ Type = imageType
+ });
+ }
+ else
+ {
+ // only allow one item of each type
+ if (itemResult.RemoteImages.Any(x => x.type == imageType))
+ {
+ break;
+ }
+
+ itemResult.RemoteImages.Add((uri.ToString(), imageType));
+ }
+
+ break;
+ }
+
default:
string readerName = reader.Name;
if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue))
@@ -977,6 +1095,92 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
+ private void FetchFromRatingsNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "rating":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
+
+ var ratingName = reader.GetAttribute("name");
+
+ using var subtree = reader.ReadSubtree();
+ FetchFromRatingNode(subtree, item, ratingName);
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+
+ private void FetchFromRatingNode(XmlReader reader, T item, string? ratingName)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "value":
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue))
+ {
+ // if ratingName contains tomato --> assume critic rating
+ if (ratingName != null &&
+ ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase) &&
+ !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase))
+ {
+ item.CriticRating = ratingValue;
+ }
+ else
+ {
+ item.CommunityRating = ratingValue;
+ }
+ }
+ }
+
+ break;
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+
/// <summary>
/// Gets the persons from XML node.
/// </summary>
@@ -1104,11 +1308,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
/// <returns>IEnumerable{System.String}.</returns>
private IEnumerable<string> SplitNames(string value)
{
- value = value ?? string.Empty;
-
// Only split by comma if there is no pipe in the string
// We have to be careful to not split names like Matthew, Jr.
- var separator = value.IndexOf('|', StringComparison.Ordinal) == -1 && value.IndexOf(';', StringComparison.Ordinal) == -1
+ var separator = !value.Contains('|', StringComparison.Ordinal) && !value.Contains(';', StringComparison.Ordinal)
? new[] { ',' }
: new[] { '|', ';' };
@@ -1116,5 +1318,25 @@ namespace MediaBrowser.XbmcMetadata.Parsers
return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : value.Split(separator, StringSplitOptions.RemoveEmptyEntries);
}
+
+ /// <summary>
+ /// Parses the ImageType from the nfo aspect property.
+ /// </summary>
+ /// <param name="aspect">The nfo aspect property.</param>
+ /// <returns>The image type.</returns>
+ private static ImageType GetImageType(string aspect)
+ {
+ return aspect switch
+ {
+ "banner" => ImageType.Banner,
+ "clearlogo" => ImageType.Logo,
+ "discart" => ImageType.Disc,
+ "landscape" => ImageType.Thumb,
+ "clearart" => ImageType.Art,
+ "fanart" => ImageType.Backdrop,
+ // unknown type (including "poster") --> primary
+ _ => ImageType.Primary,
+ };
+ }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index 81774b873..6b1607530 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -19,11 +20,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeNfoParser"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
- public EpisodeNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, config, providerManager)
+ /// <param name="logger">Instance of the <see cref="ILogger{BaseNfoParser}"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ public EpisodeNfoParser(
+ ILogger logger,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
+ : base(logger, config, providerManager, userManager, userDataManager, directoryService)
{
}
@@ -35,19 +45,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
item.ResetPeople();
- var xml = streamReader.ReadToEnd();
+ var xmlFile = streamReader.ReadToEnd();
var srch = "</episodedetails>";
- var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+ var index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+
+ var xml = xmlFile;
if (index != -1)
{
- xml = xml.Substring(0, index + srch.Length);
+ xml = xmlFile.Substring(0, index + srch.Length);
+ xmlFile = xmlFile.Substring(index + srch.Length);
}
// These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
try
{
+ // Extract episode details from the first episodedetails block
using (var stringReader = new StringReader(xml))
using (var reader = XmlReader.Create(stringReader, settings))
{
@@ -69,6 +83,25 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
}
+
+ // Extract the last episode number from nfo
+ // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
+ while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
+ {
+ xml = xmlFile.Substring(0, index + srch.Length);
+ xmlFile = xmlFile.Substring(index + srch.Length);
+
+ using (var stringReader = new StringReader(xml))
+ using (var reader = XmlReader.Create(stringReader, settings))
+ {
+ reader.MoveToContent();
+
+ if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num))
+ {
+ item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
+ }
+ }
+ }
}
catch (XmlException)
{
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index 33b0ae887..e51055725 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -5,6 +5,7 @@ using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -19,11 +20,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
/// <summary>
/// Initializes a new instance of the <see cref="MovieNfoParser"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
- public MovieNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, config, providerManager)
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="DirectoryService"/> interface.</param>
+ public MovieNfoParser(
+ ILogger logger,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
+ : base(logger, config, providerManager, userManager, userDataManager, directoryService)
{
}
@@ -39,12 +49,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
case "id":
{
+ // get ids from attributes
string? imdbId = reader.GetAttribute("IMDB");
string? tmdbId = reader.GetAttribute("TMDB");
- if (string.IsNullOrWhiteSpace(imdbId))
+ // read id from content
+ var contentId = reader.ReadElementContentAsString();
+ if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId))
{
- imdbId = reader.ReadElementContentAsString();
+ imdbId = contentId;
+ }
+ else if (string.IsNullOrEmpty(tmdbId))
+ {
+ tmdbId = contentId;
}
if (!string.IsNullOrWhiteSpace(imdbId))
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
index d8cd88b9a..2f5fd40e2 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
@@ -2,6 +2,7 @@ using System.Globalization;
using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -15,11 +16,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
/// <summary>
/// Initializes a new instance of the <see cref="SeasonNfoParser"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
- public SeasonNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, config, providerManager)
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="DirectoryService"/> interface.</param>
+ public SeasonNfoParser(
+ ILogger logger,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
+ : base(logger, config, providerManager, userManager, userDataManager, directoryService)
{
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index 74a724989..2c893ac9f 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -17,11 +17,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
/// <summary>
/// Initializes a new instance of the <see cref="SeriesNfoParser"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
- public SeriesNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, config, providerManager)
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ public SeriesNfoParser(
+ ILogger logger,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
+ : base(logger, config, providerManager, userManager, userDataManager, directoryService)
{
}
@@ -29,9 +38,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
protected override bool SupportsUrlAfterClosingXmlTag => true;
/// <inheritdoc />
- protected override string MovieDbParserSearchString => "themoviedb.org/tv/";
-
- /// <inheritdoc />
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Series> itemResult)
{
var item = itemResult.Item;
diff --git a/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs b/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs
index b3e2f2717..cfe06b940 100644
--- a/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.XbmcMetadata.Tests")]
// 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
diff --git a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
index 433a936d9..f7a85a07e 100644
--- a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
@@ -2,6 +2,7 @@ using System.IO;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.XbmcMetadata.Parsers;
@@ -17,34 +18,46 @@ namespace MediaBrowser.XbmcMetadata.Providers
private readonly ILogger<AlbumNfoProvider> _logger;
private readonly IConfigurationManager _config;
private readonly IProviderManager _providerManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="AlbumNfoProvider"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{AlbumNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
public AlbumNfoProvider(
ILogger<AlbumNfoProvider> logger,
IFileSystem fileSystem,
IConfigurationManager config,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
: base(fileSystem)
{
_logger = logger;
_config = config;
_providerManager = providerManager;
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
}
/// <inheritdoc />
protected override void Fetch(MetadataResult<MusicAlbum> result, string path, CancellationToken cancellationToken)
{
- new BaseNfoParser<MusicAlbum>(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
+ new BaseNfoParser<MusicAlbum>(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken);
}
/// <inheritdoc />
- protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
=> directoryService.GetFile(Path.Combine(info.Path, "album.nfo"));
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
index d69cdc90a..2b563b7fa 100644
--- a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
@@ -2,6 +2,7 @@ using System.IO;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.XbmcMetadata.Parsers;
@@ -17,34 +18,46 @@ namespace MediaBrowser.XbmcMetadata.Providers
private readonly ILogger<ArtistNfoProvider> _logger;
private readonly IConfigurationManager _config;
private readonly IProviderManager _providerManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="ArtistNfoProvider"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{ArtistNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
public ArtistNfoProvider(
IFileSystem fileSystem,
ILogger<ArtistNfoProvider> logger,
IConfigurationManager config,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
: base(fileSystem)
{
_logger = logger;
_config = config;
_providerManager = providerManager;
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
}
/// <inheritdoc />
protected override void Fetch(MetadataResult<MusicArtist> result, string path, CancellationToken cancellationToken)
{
- new BaseNfoParser<MusicArtist>(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
+ new BaseNfoParser<MusicArtist>(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken);
}
/// <inheritdoc />
- protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
=> directoryService.GetFile(Path.Combine(info.Path, "artist.nfo"));
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
index e7aa3ca07..8574be3f3 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.XbmcMetadata.Parsers;
@@ -18,17 +19,26 @@ namespace MediaBrowser.XbmcMetadata.Providers
private readonly ILogger<BaseVideoNfoProvider<T>> _logger;
private readonly IConfigurationManager _config;
private readonly IProviderManager _providerManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
- public BaseVideoNfoProvider(
+ protected BaseVideoNfoProvider(
ILogger<BaseVideoNfoProvider<T>> logger,
IFileSystem fileSystem,
IConfigurationManager config,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
: base(fileSystem)
{
_logger = logger;
_config = config;
_providerManager = providerManager;
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
}
/// <inheritdoc />
@@ -38,10 +48,12 @@ namespace MediaBrowser.XbmcMetadata.Providers
{
Item = result.Item
};
- new MovieNfoParser(_logger, _config, _providerManager).Fetch(tmpItem, path, cancellationToken);
+ new MovieNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(tmpItem, path, cancellationToken);
result.Item = (T)tmpItem.Item;
result.People = tmpItem.People;
+ result.Images = tmpItem.Images;
+ result.RemoteImages = tmpItem.RemoteImages;
if (tmpItem.UserDataList != null)
{
diff --git a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
index 26983b1a6..f6c5877b4 100644
--- a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
@@ -2,6 +2,7 @@ using System.IO;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.XbmcMetadata.Parsers;
@@ -17,34 +18,46 @@ namespace MediaBrowser.XbmcMetadata.Providers
private readonly ILogger<EpisodeNfoProvider> _logger;
private readonly IConfigurationManager _config;
private readonly IProviderManager _providerManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeNfoProvider"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{EpisodeNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
public EpisodeNfoProvider(
ILogger<EpisodeNfoProvider> logger,
IFileSystem fileSystem,
IConfigurationManager config,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
: base(fileSystem)
{
_logger = logger;
_config = config;
_providerManager = providerManager;
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
}
/// <inheritdoc />
protected override void Fetch(MetadataResult<Episode> result, string path, CancellationToken cancellationToken)
{
- new EpisodeNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
+ new EpisodeNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken);
}
/// <inheritdoc />
- protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
{
var path = Path.ChangeExtension(info.Path, ".nfo");
diff --git a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs
index e3f6bada1..cdbc5a918 100644
--- a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs
@@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -14,16 +15,22 @@ namespace MediaBrowser.XbmcMetadata.Providers
/// <summary>
/// Initializes a new instance of the <see cref="MovieNfoProvider"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{MovieNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
public MovieNfoProvider(
ILogger<MovieNfoProvider> logger,
IFileSystem fileSystem,
IConfigurationManager config,
- IProviderManager providerManager)
- : base(logger, fileSystem, config, providerManager)
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
+ : base(logger, fileSystem, config, providerManager, userManager, userDataManager, directoryService)
{
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs
index b490a7120..9d1f3e61d 100644
--- a/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs
@@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -14,12 +15,22 @@ namespace MediaBrowser.XbmcMetadata.Providers
/// <summary>
/// Initializes a new instance of the <see cref="MusicVideoNfoProvider"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
- public MusicVideoNfoProvider(ILogger<MusicVideoNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, fileSystem, config, providerManager)
+ /// <param name="logger">Instance of the <see cref="ILogger{SeasonNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ public MusicVideoNfoProvider(
+ ILogger<MusicVideoNfoProvider> logger,
+ IFileSystem fileSystem,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
+ : base(logger, fileSystem, config, providerManager, userManager, userDataManager, directoryService)
{
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
index 0603fd0d1..d01abadf6 100644
--- a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
@@ -2,6 +2,7 @@ using System.IO;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.XbmcMetadata.Parsers;
@@ -17,34 +18,46 @@ namespace MediaBrowser.XbmcMetadata.Providers
private readonly ILogger<SeasonNfoProvider> _logger;
private readonly IConfigurationManager _config;
private readonly IProviderManager _providerManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="SeasonNfoProvider"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{SeasonNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
public SeasonNfoProvider(
ILogger<SeasonNfoProvider> logger,
IFileSystem fileSystem,
IConfigurationManager config,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
: base(fileSystem)
{
_logger = logger;
_config = config;
_providerManager = providerManager;
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
}
/// <inheritdoc />
protected override void Fetch(MetadataResult<Season> result, string path, CancellationToken cancellationToken)
{
- new SeasonNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
+ new SeasonNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken);
}
/// <inheritdoc />
- protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
=> directoryService.GetFile(Path.Combine(info.Path, "season.nfo"));
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
index 7e059e0aa..002b94463 100644
--- a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
@@ -2,6 +2,7 @@ using System.IO;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.XbmcMetadata.Parsers;
@@ -17,34 +18,46 @@ namespace MediaBrowser.XbmcMetadata.Providers
private readonly ILogger<SeriesNfoProvider> _logger;
private readonly IConfigurationManager _config;
private readonly IProviderManager _providerManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="SeriesNfoProvider"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{SeriesNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
public SeriesNfoProvider(
ILogger<SeriesNfoProvider> logger,
IFileSystem fileSystem,
IConfigurationManager config,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
: base(fileSystem)
{
_logger = logger;
_config = config;
_providerManager = providerManager;
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
}
/// <inheritdoc />
protected override void Fetch(MetadataResult<Series> result, string path, CancellationToken cancellationToken)
{
- new SeriesNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken);
+ new SeriesNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken);
}
/// <inheritdoc />
- protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
=> directoryService.GetFile(Path.Combine(info.Path, "tvshow.nfo"));
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs
index f66ad30ca..93b1be62f 100644
--- a/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs
@@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -14,12 +15,22 @@ namespace MediaBrowser.XbmcMetadata.Providers
/// <summary>
/// Initializes a new instance of the <see cref="VideoNfoProvider"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="config">the configuration manager.</param>
- /// <param name="providerManager">The provider manager.</param>
- public VideoNfoProvider(ILogger<VideoNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager)
- : base(logger, fileSystem, config, providerManager)
+ /// <param name="logger">Instance of the <see cref="ILogger{VideoNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ public VideoNfoProvider(
+ ILogger<VideoNfoProvider> logger,
+ IFileSystem fileSystem,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
+ : base(logger, fileSystem, config, providerManager, userManager, userDataManager, directoryService)
{
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
index c22f77dcd..2385e7048 100644
--- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs
@@ -96,18 +96,16 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
/// <inheritdoc />
- protected override List<string> GetTagsUsed(BaseItem item)
+ protected override IEnumerable<string> GetTagsUsed(BaseItem item)
{
- var list = base.GetTagsUsed(item);
- list.AddRange(
- new string[]
- {
- "track",
- "artist",
- "albumartist"
- });
+ foreach (var tag in base.GetTagsUsed(item))
+ {
+ yield return tag;
+ }
- return list;
+ yield return "track";
+ yield return "artist";
+ yield return "albumartist";
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
index 6365cdecb..71b58cddb 100644
--- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
@@ -88,16 +88,15 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
/// <inheritdoc />
- protected override List<string> GetTagsUsed(BaseItem item)
+ protected override IEnumerable<string> GetTagsUsed(BaseItem item)
{
- var list = base.GetTagsUsed(item);
- list.AddRange(new string[]
+ foreach (var tag in base.GetTagsUsed(item))
{
- "album",
- "disbanded"
- });
+ yield return tag;
+ }
- return list;
+ yield return "album";
+ yield return "disbanded";
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 9f22e618e..3be35e2d9 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -166,19 +166,16 @@ namespace MediaBrowser.XbmcMetadata.Savers
/// <inheritdoc />
public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType);
- protected virtual List<string> GetTagsUsed(BaseItem item)
+ protected virtual IEnumerable<string> GetTagsUsed(BaseItem item)
{
- var list = new List<string>();
foreach (var providerKey in item.ProviderIds.Keys)
{
var providerIdTagName = GetTagForProviderKey(providerKey);
if (!_commonTags.Contains(providerIdTagName))
{
- list.Add(providerIdTagName);
+ yield return providerIdTagName;
}
}
-
- return list;
}
/// <inheritdoc />
@@ -261,7 +258,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
AddMediaInfo(hasMediaSources, writer);
}
- var tagsUsed = GetTagsUsed(item);
+ var tagsUsed = GetTagsUsed(item).ToList();
try
{
@@ -351,10 +348,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
var scanType = stream.IsInterlaced ? "interlaced" : "progressive";
- if (!string.IsNullOrEmpty(scanType))
- {
- writer.WriteElementString("scantype", scanType);
- }
+ writer.WriteElementString("scantype", scanType);
if (stream.Channels.HasValue)
{
@@ -465,7 +459,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (item.LockedFields.Length > 0)
{
- writer.WriteElementString("lockedfields", string.Join("|", item.LockedFields));
+ writer.WriteElementString("lockedfields", string.Join('|', item.LockedFields));
}
writer.WriteElementString("dateadded", item.DateCreated.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
@@ -968,7 +962,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
=> string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
|| string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
- private void AddCustomTags(string path, List<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseNfoSaver> logger)
+ private void AddCustomTags(string path, IReadOnlyCollection<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseNfoSaver> logger)
{
var settings = new XmlReaderSettings()
{
diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
index 5d3d17893..62f80e81b 100644
--- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
@@ -111,24 +111,23 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
/// <inheritdoc />
- protected override List<string> GetTagsUsed(BaseItem item)
+ protected override IEnumerable<string> GetTagsUsed(BaseItem item)
{
- var list = base.GetTagsUsed(item);
- list.AddRange(new string[]
+ foreach (var tag in base.GetTagsUsed(item))
{
- "aired",
- "season",
- "episode",
- "episodenumberend",
- "airsafter_season",
- "airsbefore_episode",
- "airsbefore_season",
- "displayseason",
- "displayepisode",
- "showtitle"
- });
-
- return list;
+ yield return tag;
+ }
+
+ yield return "aired";
+ yield return "season";
+ yield return "episode";
+ yield return "episodenumberend";
+ yield return "airsafter_season";
+ yield return "airsbefore_episode";
+ yield return "airsbefore_season";
+ yield return "displayseason";
+ yield return "displayepisode";
+ yield return "showtitle";
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index 841121735..412e8031b 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -123,18 +123,17 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
/// <inheritdoc />
- protected override List<string> GetTagsUsed(BaseItem item)
+ protected override IEnumerable<string> GetTagsUsed(BaseItem item)
{
- var list = base.GetTagsUsed(item);
- list.AddRange(new string[]
+ foreach (var tag in base.GetTagsUsed(item))
{
- "album",
- "artist",
- "set",
- "id"
- });
+ yield return tag;
+ }
- return list;
+ yield return "album";
+ yield return "artist";
+ yield return "set";
+ yield return "id";
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
index 925a230bd..b9d73ba82 100644
--- a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
@@ -72,15 +72,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
/// <inheritdoc />
- protected override List<string> GetTagsUsed(BaseItem item)
+ protected override IEnumerable<string> GetTagsUsed(BaseItem item)
{
- var list = base.GetTagsUsed(item);
- list.AddRange(new string[]
+ foreach (var tag in base.GetTagsUsed(item))
{
- "seasonnumber"
- });
+ yield return tag;
+ }
- return list;
+ yield return "seasonnumber";
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
index 42285db76..083f22e5d 100644
--- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs
@@ -90,20 +90,19 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
/// <inheritdoc />
- protected override List<string> GetTagsUsed(BaseItem item)
+ protected override IEnumerable<string> GetTagsUsed(BaseItem item)
{
- var list = base.GetTagsUsed(item);
- list.AddRange(new string[]
+ foreach (var tag in base.GetTagsUsed(item))
{
- "id",
- "episodeguide",
- "season",
- "episode",
- "status",
- "displayorder"
- });
+ yield return tag;
+ }
- return list;
+ yield return "id";
+ yield return "episodeguide";
+ yield return "season";
+ yield return "episode";
+ yield return "status";
+ yield return "displayorder";
}
}
}
diff --git a/README.md b/README.md
index 29f992349..6859a8a76 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,6 @@
<a href="https://features.jellyfin.org">
<img alt="Submit Feature Requests" src="https://img.shields.io/badge/fider-vote%20on%20features-success.svg"/>
</a>
-<a href="https://forum.jellyfin.org">
-<img alt="Discuss on our Forum" src="https://img.shields.io/discourse/https/forum.jellyfin.org/users.svg"/>
-</a>
<a href="https://matrix.to/#/+jellyfin:matrix.org">
<img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix"/>
</a>
@@ -85,6 +82,8 @@ Before the project can be built, you must first install the [.NET 5.0 SDK](https
Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download).
+[ffmpeg](https://github.com/jellyfin/jellyfin-ffmpeg) will also need to be installed.
+
### Cloning the Repository
After dependencies are installed you will need to clone a local copy of this repository. If you just want to run the server from source you can clone this repository directly, but if you are intending to contribute code changes to the project, you should [set up your own fork](https://jellyfin.org/docs/general/contributing/development.html#set-up-your-copy-of-the-repo) of the repository. The following example shows how you can clone the repository directly over HTTPS.
diff --git a/RSSDP/DeviceAvailableEventArgs.cs b/RSSDP/DeviceAvailableEventArgs.cs
index b7d22a7df..04b14c4dc 100644
--- a/RSSDP/DeviceAvailableEventArgs.cs
+++ b/RSSDP/DeviceAvailableEventArgs.cs
@@ -8,7 +8,7 @@ namespace Rssdp
/// </summary>
public sealed class DeviceAvailableEventArgs : EventArgs
{
- public IPAddress LocalIpAddress { get; set; }
+ public IPAddress RemoteIpAddress { get; set; }
private readonly DiscoveredSsdpDevice _DiscoveredDevice;
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index 8f1f0fa61..e49c0e77b 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -42,7 +43,7 @@ namespace Rssdp.Infrastructure
private HttpResponseParser _ResponseParser;
private readonly ILogger _logger;
private ISocketFactory _SocketFactory;
- private readonly INetworkManager _networkManager;
+ private readonly INetworkManager _networkManager;
private int _LocalPort;
private int _MulticastTtl;
@@ -68,7 +69,7 @@ namespace Rssdp.Infrastructure
INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
{
-
+
}
/// <summary>
@@ -358,7 +359,7 @@ namespace Rssdp.Infrastructure
{
// Not support IPv6 right now
continue;
- }
+ }
try
{
@@ -415,10 +416,7 @@ namespace Rssdp.Infrastructure
{
lock (_SendSocketSynchroniser)
{
- if (_sendSockets == null)
- {
- _sendSockets = CreateSocketAndListenForResponsesAsync();
- }
+ _sendSockets ??= CreateSocketAndListenForResponsesAsync();
}
}
}
diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs
index bfad6de97..188e298e2 100644
--- a/RSSDP/SsdpDeviceLocator.cs
+++ b/RSSDP/SsdpDeviceLocator.cs
@@ -97,6 +97,11 @@ namespace Rssdp.Infrastructure
private async void OnBroadcastTimerCallback(object state)
{
+ if (IsDisposed)
+ {
+ return;
+ }
+
StartListeningForNotifications();
RemoveExpiredDevicesFromCache();
@@ -180,8 +185,6 @@ namespace Rssdp.Infrastructure
/// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> ty is true.</exception>
public void StartListeningForNotifications()
{
- ThrowIfDisposed();
-
_CommunicationsServer.RequestReceived -= CommsServer_RequestReceived;
_CommunicationsServer.RequestReceived += CommsServer_RequestReceived;
_CommunicationsServer.BeginListeningForBroadcasts();
@@ -208,7 +211,7 @@ namespace Rssdp.Infrastructure
/// Raises the <see cref="DeviceAvailable"/> event.
/// </summary>
/// <seealso cref="DeviceAvailable"/>
- protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress localIpAddress)
+ protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IpAddress)
{
if (this.IsDisposed)
{
@@ -220,7 +223,7 @@ namespace Rssdp.Infrastructure
{
handlers(this, new DeviceAvailableEventArgs(device, isNewDevice)
{
- LocalIpAddress = localIpAddress
+ RemoteIpAddress = IpAddress
});
}
}
@@ -286,7 +289,7 @@ namespace Rssdp.Infrastructure
}
}
- private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress localIpAddress)
+ private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress IpAddress)
{
bool isNewDevice = false;
lock (_Devices)
@@ -304,17 +307,17 @@ namespace Rssdp.Infrastructure
}
}
- DeviceFound(device, isNewDevice, localIpAddress);
+ DeviceFound(device, isNewDevice, IpAddress);
}
- private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress localIpAddress)
+ private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IpAddress)
{
if (!NotificationTypeMatchesFilter(device))
{
return;
}
- OnDeviceAvailable(device, isNewDevice, localIpAddress);
+ OnDeviceAvailable(device, isNewDevice, IpAddress);
}
private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device)
@@ -347,13 +350,13 @@ namespace Rssdp.Infrastructure
return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
}
- private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress localIpAddress)
+ private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress IpAddress)
{
if (!message.IsSuccessStatusCode)
{
return;
}
-
+
var location = GetFirstHeaderUriValue("Location", message);
if (location != null)
{
@@ -367,11 +370,11 @@ namespace Rssdp.Infrastructure
ResponseHeaders = message.Headers
};
- AddOrUpdateDiscoveredDevice(device, localIpAddress);
+ AddOrUpdateDiscoveredDevice(device, IpAddress);
}
}
- private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress localIpAddress)
+ private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IpAddress)
{
if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
{
@@ -381,7 +384,7 @@ namespace Rssdp.Infrastructure
var notificationType = GetFirstHeaderStringValue("NTS", message);
if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
{
- ProcessAliveNotification(message, localIpAddress);
+ ProcessAliveNotification(message, IpAddress);
}
else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
{
@@ -389,7 +392,7 @@ namespace Rssdp.Infrastructure
}
}
- private void ProcessAliveNotification(HttpRequestMessage message, IPAddress localIpAddress)
+ private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IpAddress)
{
var location = GetFirstHeaderUriValue("Location", message);
if (location != null)
@@ -404,7 +407,7 @@ namespace Rssdp.Infrastructure
ResponseHeaders = message.Headers
};
- AddOrUpdateDiscoveredDevice(device, localIpAddress);
+ AddOrUpdateDiscoveredDevice(device, IpAddress);
}
}
@@ -515,11 +518,6 @@ namespace Rssdp.Infrastructure
private void RemoveExpiredDevicesFromCache()
{
- if (this.IsDisposed)
- {
- return;
- }
-
DiscoveredSsdpDevice[] expiredDevices = null;
lock (_Devices)
{
@@ -630,7 +628,7 @@ namespace Rssdp.Infrastructure
private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e)
{
- ProcessNotificationMessage(e.Message, e.LocalIpAddress);
+ ProcessNotificationMessage(e.Message, e.ReceivedFrom.Address);
}
}
}
diff --git a/apiclient/.openapi-generator-ignore b/apiclient/.openapi-generator-ignore
deleted file mode 100644
index f3802cf54..000000000
--- a/apiclient/.openapi-generator-ignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# Prevent generator from creating these files:
-git_push.sh
diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh
deleted file mode 100644
index 9599f85db..000000000
--- a/apiclient/templates/typescript/axios/generate.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-artifactsDirectory="${1}"
-
-java -jar openapi-generator-cli.jar generate \
- --input-spec ${artifactsDirectory}/openapispec/openapi.json \
- --generator-name typescript-axios \
- --output ./apiclient/generated/typescript/axios \
- --template-dir ./apiclient/templates/typescript/axios \
- --ignore-file-override ./apiclient/.openapi-generator-ignore \
- --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",modelPackage="models",apiPackage="api",npmName="axios"
diff --git a/apiclient/templates/typescript/axios/package.mustache b/apiclient/templates/typescript/axios/package.mustache
deleted file mode 100644
index 7bfab08cb..000000000
--- a/apiclient/templates/typescript/axios/package.mustache
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "name": "@jellyfin/client-axios",
- "version": "10.7.0{{snapshotVersion}}",
- "description": "Jellyfin api client using axios",
- "author": "Jellyfin Contributors",
- "keywords": [
- "axios",
- "typescript",
- "jellyfin"
- ],
- "license": "GPL-3.0-only",
- "main": "./dist/index.js",
- "typings": "./dist/index.d.ts",
- "scripts": {
- "build": "tsc --outDir dist/",
- "prepublishOnly": "npm run build"
- },
- "dependencies": {
- "axios": "^0.19.2"
- },
- "devDependencies": {
- "@types/node": "^12.11.5",
- "typescript": "^3.6.4"
- }{{#npmRepository}},{{/npmRepository}}
-{{#npmRepository}}
- "publishConfig": {
- "registry": "{{npmRepository}}"
- }
-{{/npmRepository}}
-}
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
index 7cbfa88ee..9ebaf2bd8 100644
--- a/debian/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -33,6 +33,11 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# [OPTIONAL] run Jellyfin without the web app
#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
+# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
+# 0 = Workstation
+# 1 = Server
+#COMPlus_gcServer=1
+
#
# SysV init/Upstart options
#
diff --git a/debian/postinst b/debian/postinst
index 860222e05..2f9c4cffb 100644
--- a/debian/postinst
+++ b/debian/postinst
@@ -9,6 +9,8 @@ if [[ -f $DEFAULT_FILE ]]; then
. $DEFAULT_FILE
fi
+JELLYFIN_USER=${JELLYFIN_USER:-jellyfin}
+
# Data directories for program data (cache, db), configs, and logs
PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
@@ -18,12 +20,12 @@ CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
case "$1" in
configure)
# create jellyfin group if it does not exist
- if [[ -z "$(getent group jellyfin)" ]]; then
- addgroup --quiet --system jellyfin > /dev/null 2>&1
+ if [[ -z "$(getent group ${JELLYFIN_USER})" ]]; then
+ addgroup --quiet --system ${JELLYFIN_USER} > /dev/null 2>&1
fi
# create jellyfin user if it does not exist
- if [[ -z "$(getent passwd jellyfin)" ]]; then
- adduser --system --ingroup jellyfin --shell /bin/false jellyfin --no-create-home --home ${PROGRAMDATA} \
+ if [[ -z "$(getent passwd ${JELLYFIN_USER})" ]]; then
+ adduser --system --ingroup ${JELLYFIN_USER} --shell /bin/false ${JELLYFIN_USER} --no-create-home --home ${PROGRAMDATA} \
--gecos "Jellyfin default user" > /dev/null 2>&1
fi
# ensure $PROGRAMDATA exists
@@ -43,7 +45,7 @@ case "$1" in
mkdir $CACHEDATA
fi
# Ensure permissions are correct on all config directories
- chown -R jellyfin $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
+ chown -R ${JELLYFIN_USER} $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
chgrp adm $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
chmod 0750 $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index 428072613..4c426b6d5 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index b540efc09..7ed6d52bc 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index 426ce02fc..b46cceaa4 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 2549f25ee..137e56ecf 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -13,9 +13,7 @@ RUN dnf update -y \
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd
# Install DotNET SDK
-RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \
- && curl -o /etc/yum.repos.d/microsoft-prod.repo https://packages.microsoft.com/config/fedora/$(rpm -E %fedora)/prod.repo \
- && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
+RUN dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
# Create symlinks and directories
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 3b91515f3..a0e23557a 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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-musl b/deployment/Dockerfile.linux.amd64-musl
index 2ca9072ba..af0f55f8e 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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.arm64 b/deployment/Dockerfile.linux.arm64
index 03efd306d..ba004bb6a 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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.armhf b/deployment/Dockerfile.linux.armhf
index 585572204..0d1114c01 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index b37afdcfb..b57dc53f5 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 686b20197..3783dfacf 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 3513bf8ec..663a7af9e 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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 5acdf0d17..83eb24e42 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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 42f757d05..1187f37b9 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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 6ed1193fb..8b2361f0b 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/fedora/jellyfin.env b/fedora/jellyfin.env
index bf64acd3f..56b7a3558 100644
--- a/fedora/jellyfin.env
+++ b/fedora/jellyfin.env
@@ -35,3 +35,7 @@ JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
# [OPTIONAL] run Jellyfin without the web app
#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
+# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
+# 0 = Workstation
+# 1 = Server
+#COMPlus_gcServer=1
diff --git a/fuzz/.gitignore b/fuzz/.gitignore
new file mode 100644
index 000000000..652de0a45
--- /dev/null
+++ b/fuzz/.gitignore
@@ -0,0 +1 @@
+Findings
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
new file mode 100644
index 000000000..791cb140d
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="Emby.Server.Implementations">
+ <HintPath>Emby.Server.Implementations.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="SharpFuzz" Version="1.6.2" />
+ </ItemGroup>
+
+</Project>
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
new file mode 100644
index 000000000..a4a6f5f54
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
@@ -0,0 +1,32 @@
+using System;
+using Emby.Server.Implementations.Library;
+using SharpFuzz;
+
+namespace Emby.Server.Implementations.Fuzz
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ switch (args[0])
+ {
+ case "PathExtensions.TryReplaceSubPath": Run(PathExtensions_TryReplaceSubPath); return;
+ default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}");
+ }
+ }
+
+ private static void Run(Action<string> action) => Fuzzer.OutOfProcess.Run(action);
+
+ private static void PathExtensions_TryReplaceSubPath(string data)
+ {
+ // Stupid, but it worked
+ var parts = data.Split(':');
+ if (parts.Length != 3)
+ {
+ return;
+ }
+
+ _ = PathExtensions.TryReplaceSubPath(parts[0], parts[1], parts[2], out _);
+ }
+ }
+}
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt
new file mode 100644
index 000000000..aacf973d6
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt
@@ -0,0 +1 @@
+/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt/:/home/bond/dev/jellyfin/:/srv/jellyfin/
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
new file mode 100755
index 000000000..244f73402
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+set -e
+
+dotnet build -c Release ../../Emby.Server.Implementations/Emby.Server.Implementations.csproj --output bin
+sharpfuzz bin/Emby.Server.Implementations.dll
+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/net5.0/Emby.Server.Implementations.Fuzz.dll "$1"
diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
new file mode 100644
index 000000000..6fcfbae0e
--- /dev/null
+++ b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="Jellyfin.Server">
+ <HintPath>jellyfin.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="SharpFuzz" Version="1.6.2" />
+ </ItemGroup>
+
+</Project>
diff --git a/fuzz/Jellyfin.Server.Fuzz/Program.cs b/fuzz/Jellyfin.Server.Fuzz/Program.cs
new file mode 100644
index 000000000..e47286c13
--- /dev/null
+++ b/fuzz/Jellyfin.Server.Fuzz/Program.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Server.Middleware;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+using SharpFuzz;
+
+namespace Emby.Server.Implementations.Fuzz
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ switch (args[0])
+ {
+ case "UrlDecodeQueryFeature": Run(UrlDecodeQueryFeature); return;
+ default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}");
+ }
+ }
+
+ private static void Run(Action<string> action) => Fuzzer.OutOfProcess.Run(action);
+
+ private static void UrlDecodeQueryFeature(string data)
+ {
+ var dict = new Dictionary<string, StringValues>
+ {
+ { data, StringValues.Empty }
+ };
+ _ = new UrlDecodeQueryFeature(new QueryFeature(new QueryCollection(dict)));
+ }
+ }
+}
diff --git a/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt b/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
new file mode 100644
index 000000000..73f356b93
--- /dev/null
+++ b/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
@@ -0,0 +1 @@
+a%3D1%26b%3D2%26c%3D3
diff --git a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh
new file mode 100755
index 000000000..ad81e2c35
--- /dev/null
+++ b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh
@@ -0,0 +1,11 @@
+#!/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/net5.0/Jellyfin.Server.Fuzz.dll "$1"
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 371f02566..44bc34369 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -1,12 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
- <!-- disable warning SA1202: 'public' members must come before 'private' members -->
- <Rule Id="SA1202" Action="Info" />
- <!-- disable warning SA1204: Static members must appear before non-static members -->
- <Rule Id="SA1204" Action="Info" />
- <!-- disable warning SA1404: Code analysis suppression should have justification -->
- <Rule Id="SA1404" Action="Info" />
+ <!-- disable warning CA1040: Avoid empty interfaces -->
+ <Rule Id="CA1040" Action="Info" />
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
@@ -22,6 +18,10 @@
<Rule Id="SA1130" Action="None" />
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
<Rule Id="SA1200" Action="None" />
+ <!-- disable warning SA1202: 'public' members must come before 'private' members -->
+ <Rule Id="SA1202" Action="None" />
+ <!-- disable warning SA1204: Static members must appear before non-static members -->
+ <Rule Id="SA1204" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
@@ -32,11 +32,19 @@
<Rule Id="SA1515" Action="None" />
<!-- disable warning SA1600: Elements should be documented -->
<Rule Id="SA1600" Action="None" />
+ <!-- disable warning SA1602: Enumeration items should be documented -->
+ <Rule Id="SA1602" Action="None" />
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
<Rule Id="SA1633" Action="None" />
</Rules>
- <Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
+ <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
+ <!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
+ or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
+ <Rule Id="CA2016" Action="Error" />
+
+ <!-- disable warning CA1024: Use properties where appropriate -->
+ <Rule Id="CA1024" Action="Info" />
<!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
@@ -47,6 +55,8 @@
<Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
+ <!-- disable warning CA1805: Do not initialize unnecessarily -->
+ <Rule Id="CA1805" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
If so, remove the code from the assembly.
If this class is intended to contain only static members, make it static -->
@@ -55,7 +65,11 @@
<Rule Id="CA1822" Action="Info" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="Info" />
+ <!-- disable warning CA5394: Do not use insecure randomness -->
+ <Rule Id="CA5394" Action="Info" />
+ <!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute -->
+ <Rule Id="CA1014" Action="Info" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
<!-- disable warning CA1055: URI return values should not be strings -->
@@ -66,5 +80,7 @@
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
+ <!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments -->
+ <Rule Id="CA2101" Action="None" />
</Rules>
</RuleSet>
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index ee20cc573..de03aa5f5 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -128,6 +128,8 @@ namespace Jellyfin.Api.Tests.Auth
{
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
authorizationInfo.User = _fixture.Create<User>();
+ authorizationInfo.User.AddDefaultPermissions();
+ authorizationInfo.User.AddDefaultPreferences();
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
authorizationInfo.IsApiKey = false;
diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
index 09ffa8468..5b3d784ff 100644
--- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Net;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
@@ -41,7 +42,7 @@ namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed)
{
_networkManagerMock
- .Setup(n => n.IsInLocalNetwork(It.IsAny<string>()))
+ .Setup(n => n.IsInLocalNetwork(It.IsAny<IPAddress>()))
.Returns(isInLocalNetwork);
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
diff --git a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
new file mode 100644
index 000000000..117083815
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Api.Controllers;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Controllers
+{
+ public class DynamicHlsControllerTests
+ {
+ [Theory]
+ [MemberData(nameof(GetSegmentLengths_Success_TestData))]
+ public void GetSegmentLengths_Success(long runtimeTicks, int segmentlength, double[] expected)
+ {
+ var res = DynamicHlsController.GetSegmentLengthsInternal(runtimeTicks, segmentlength);
+ Assert.Equal(expected.Length, res.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i], res[i]);
+ }
+ }
+
+ public static IEnumerable<object[]> GetSegmentLengths_Success_TestData()
+ {
+ yield return new object[] { 0, 6, Array.Empty<double>() };
+ yield return new object[]
+ {
+ TimeSpan.FromSeconds(3).Ticks,
+ 6,
+ new double[] { 3 }
+ };
+ yield return new object[]
+ {
+ TimeSpan.FromSeconds(6).Ticks,
+ 6,
+ new double[] { 6 }
+ };
+ yield return new object[]
+ {
+ TimeSpan.FromSeconds(3.3333333).Ticks,
+ 6,
+ new double[] { 3.3333333 }
+ };
+ yield return new object[]
+ {
+ TimeSpan.FromSeconds(9.3333333).Ticks,
+ 6,
+ new double[] { 6, 3.3333333 }
+ };
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index eca3df79b..d4ea91872 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -10,35 +10,33 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.15.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
- <PackageReference Include="AutoFixture.Xunit2" Version="4.15.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.3" />
+ <PackageReference Include="AutoFixture" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
- <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
+ <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
+ <ProjectReference Include="../../Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs
index 544a74637..92c534eae 100644
--- a/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs
@@ -1,17 +1,11 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
namespace Jellyfin.Api.Tests.ModelBinders
{
public enum TestType
{
-#pragma warning disable SA1602 // Enumeration items should be documented
How,
Much,
Is,
The,
Fish
-#pragma warning restore SA1602 // Enumeration items should be documented
}
}
diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs
index f27cdf7b6..f9bca4146 100644
--- a/tests/Jellyfin.Api.Tests/TestHelpers.cs
+++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs
@@ -26,8 +26,11 @@ namespace Jellyfin.Api.Tests
{
var user = new User(
"jellyfin",
- typeof(DefaultAuthenticationProvider).FullName,
- typeof(DefaultPasswordResetProvider).FullName);
+ typeof(DefaultAuthenticationProvider).FullName!,
+ typeof(DefaultPasswordResetProvider).FullName!);
+
+ user.AddDefaultPermissions();
+ user.AddDefaultPreferences();
// Set administrator flag.
user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
diff --git a/tests/Jellyfin.Common.Tests/Crc32Tests.cs b/tests/Jellyfin.Common.Tests/Crc32Tests.cs
new file mode 100644
index 000000000..e95a2867f
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Crc32Tests.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Text;
+using MediaBrowser.Common;
+using Xunit;
+
+namespace Jellyfin.Common.Tests
+{
+ public static class Crc32Tests
+ {
+ [Fact]
+ public static void Compute_Empty_Zero()
+ {
+ Assert.Equal<uint>(0, Crc32.Compute(Array.Empty<byte>()));
+ }
+
+ [Theory]
+ [InlineData(0x414fa339, "The quick brown fox jumps over the lazy dog")]
+ public static void Compute_Valid_Success(uint expected, string data)
+ {
+ Assert.Equal(expected, Crc32.Compute(Encoding.UTF8.GetBytes(data)));
+ }
+
+ [Theory]
+ [InlineData(0x414fa339, "54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67")]
+ [InlineData(0x190a55ad, "0000000000000000000000000000000000000000000000000000000000000000")]
+ [InlineData(0xff6cab0b, "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")]
+ [InlineData(0x91267e8a, "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F")]
+ public static void Compute_ValidHex_Success(uint expected, string data)
+ {
+ Assert.Equal(expected, Crc32.Compute(Convert.FromHexString(data)));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs
new file mode 100644
index 000000000..e6c325bac
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Cryptography;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Cryptography
+{
+ public static class PasswordHashTests
+ {
+ [Fact]
+ public static void Ctor_Null_ThrowsArgumentNullException()
+ {
+ Assert.Throws<ArgumentNullException>(() => new PasswordHash(null!, Array.Empty<byte>()));
+ }
+
+ [Fact]
+ public static void Ctor_Empty_ThrowsArgumentException()
+ {
+ Assert.Throws<ArgumentException>(() => new PasswordHash(string.Empty, Array.Empty<byte>()));
+ }
+
+ public static IEnumerable<object[]> Parse_Valid_TestData()
+ {
+ // Id
+ yield return new object[]
+ {
+ "$PBKDF2",
+ new PasswordHash("PBKDF2", Array.Empty<byte>())
+ };
+
+ // Id + parameter
+ yield return new object[]
+ {
+ "$PBKDF2$iterations=1000",
+ new PasswordHash(
+ "PBKDF2",
+ Array.Empty<byte>(),
+ Array.Empty<byte>(),
+ new Dictionary<string, string>()
+ {
+ { "iterations", "1000" },
+ })
+ };
+
+ // Id + parameters
+ yield return new object[]
+ {
+ "$PBKDF2$iterations=1000,m=120",
+ new PasswordHash(
+ "PBKDF2",
+ Array.Empty<byte>(),
+ Array.Empty<byte>(),
+ new Dictionary<string, string>()
+ {
+ { "iterations", "1000" },
+ { "m", "120" }
+ })
+ };
+
+ // Id + hash
+ yield return new object[]
+ {
+ "$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
+ new PasswordHash(
+ "PBKDF2",
+ Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
+ Array.Empty<byte>(),
+ new Dictionary<string, string>())
+ };
+
+ // Id + salt + hash
+ yield return new object[]
+ {
+ "$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
+ new PasswordHash(
+ "PBKDF2",
+ Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
+ Convert.FromHexString("69F420"),
+ new Dictionary<string, string>())
+ };
+
+ // Id + parameter + hash
+ yield return new object[]
+ {
+ "$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
+ new PasswordHash(
+ "PBKDF2",
+ Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
+ Array.Empty<byte>(),
+ new Dictionary<string, string>()
+ {
+ { "iterations", "1000" }
+ })
+ };
+
+ // Id + parameters + hash
+ yield return new object[]
+ {
+ "$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
+ new PasswordHash(
+ "PBKDF2",
+ Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
+ Array.Empty<byte>(),
+ new Dictionary<string, string>()
+ {
+ { "iterations", "1000" },
+ { "m", "120" }
+ })
+ };
+
+ // Id + parameters + salt + hash
+ yield return new object[]
+ {
+ "$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
+ new PasswordHash(
+ "PBKDF2",
+ Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
+ Convert.FromHexString("69F420"),
+ new Dictionary<string, string>()
+ {
+ { "iterations", "1000" },
+ { "m", "120" }
+ })
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(Parse_Valid_TestData))]
+ public static void Parse_Valid_Success(string passwordHashString, PasswordHash expected)
+ {
+ var passwordHash = PasswordHash.Parse(passwordHashString);
+ Assert.Equal(expected.Id, passwordHash.Id);
+ Assert.Equal(expected.Parameters, passwordHash.Parameters);
+ Assert.Equal(expected.Salt.ToArray(), passwordHash.Salt.ToArray());
+ Assert.Equal(expected.Hash.ToArray(), passwordHash.Hash.ToArray());
+ Assert.Equal(expected.ToString(), passwordHash.ToString());
+ }
+
+ [Theory]
+ [InlineData("$PBKDF2")]
+ [InlineData("$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
+ [InlineData("$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
+ [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
+ [InlineData("$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
+ [InlineData("$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
+ [InlineData("$PBKDF2$iterations=1000,m=120")]
+ public static void ToString_Roundtrip_Success(string passwordHash)
+ {
+ Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString());
+ }
+
+ [Fact]
+ public static void Parse_Null_ThrowsArgumentException()
+ {
+ Assert.Throws<ArgumentException>(() => PasswordHash.Parse(null));
+ }
+
+ [Fact]
+ public static void Parse_Empty_ThrowsArgumentException()
+ {
+ Assert.Throws<ArgumentException>(() => PasswordHash.Parse(string.Empty));
+ }
+
+ [Theory]
+ [InlineData("$")] // No id
+ [InlineData("$$")] // Empty segments
+ [InlineData("PBKDF2$")] // Doesn't start with $
+ [InlineData("$PBKDF2$$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Empty segment
+ [InlineData("$PBKDF2$iterations=1000$$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Empty salt segment
+ [InlineData("$PBKDF2$iterations=1000$69F420$")] // Empty hash segment
+ [InlineData("$PBKDF2$=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
+ [InlineData("$PBKDF2$=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
+ [InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
+ [InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Ends on $
+ [InlineData("$PBKDF2$iterations=$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Extra segment
+ [InlineData("$PBKDF2$iterations=$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$anotherone")] // Extra segment
+ [InlineData("$PBKDF2$iterations=$invalidstalt$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid salt
+ [InlineData("$PBKDF2$iterations=$69F420$invalid hash")] // Invalid hash
+ [InlineData("$PBKDF2$69F420$")] // Empty hash
+ public static void Parse_InvalidFormat_ThrowsFormatException(string passwordHash)
+ {
+ Assert.Throws<FormatException>(() => PasswordHash.Parse(passwordHash));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs
new file mode 100644
index 000000000..9903409fa
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Extensions;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Extensions
+{
+ public static class CopyToExtensionsTests
+ {
+ public static IEnumerable<object[]> CopyTo_Valid_Correct_TestData()
+ {
+ yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } };
+ yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } };
+ }
+
+ [Theory]
+ [MemberData(nameof(CopyTo_Valid_Correct_TestData))]
+ public static void CopyTo_Valid_Correct<T>(IReadOnlyList<T> source, IList<T> destination, int index, IList<T> expected)
+ {
+ source.CopyTo(destination, index);
+ Assert.Equal(expected, destination);
+ }
+
+ public static IEnumerable<object[]> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
+ {
+ yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 };
+ yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 };
+ yield return new object[] { new[] { 0, 1, 2 }, Array.Empty<int>(), 0 };
+ yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 };
+ yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 };
+ }
+
+ [Theory]
+ [MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))]
+ public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException<T>(IReadOnlyList<T> source, IList<T> destination, int index)
+ {
+ Assert.Throws<ArgumentOutOfRangeException>(() => source.CopyTo(destination, index));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs
deleted file mode 100644
index 8bf613f05..000000000
--- a/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using MediaBrowser.Common.Extensions;
-using Xunit;
-
-namespace Jellyfin.Common.Tests.Extensions
-{
- public class StringExtensionsTests
- {
- [Theory]
- [InlineData("", 'q', "")]
- [InlineData("Banana split", ' ', "Banana")]
- [InlineData("Banana split", 'q', "Banana split")]
- public void LeftPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
- {
- var result = str.AsSpan().LeftPart(needle).ToString();
- Assert.Equal(expectedResult, result);
- }
-
- [Theory]
- [InlineData("", "", "")]
- [InlineData("", "q", "")]
- [InlineData("Banana split", "", "")]
- [InlineData("Banana split", " ", "Banana")]
- [InlineData("Banana split test", " split", "Banana")]
- public void LeftPart_ValidArgsWithoutStringComparison_Correct(string str, string needle, string expectedResult)
- {
- var result = str.AsSpan().LeftPart(needle).ToString();
- Assert.Equal(expectedResult, result);
- }
-
- [Theory]
- [InlineData("", "", StringComparison.Ordinal, "")]
- [InlineData("Banana split", " ", StringComparison.Ordinal, "Banana")]
- [InlineData("Banana split test", " split", StringComparison.Ordinal, "Banana")]
- [InlineData("Banana split test", " Split", StringComparison.Ordinal, "Banana split test")]
- [InlineData("Banana split test", " Splït", StringComparison.InvariantCultureIgnoreCase, "Banana split test")]
- public void LeftPart_ValidArgs_Correct(string str, string needle, StringComparison stringComparison, string expectedResult)
- {
- var result = str.AsSpan().LeftPart(needle, stringComparison).ToString();
- Assert.Equal(expectedResult, result);
- }
- }
-}
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 57edbf902..546b2487e 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -10,18 +10,20 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -32,8 +34,4 @@
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
index 9ded01f2b..7629d9912 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
@@ -1,34 +1,45 @@
-using System.Text.Json;
+using System.Globalization;
+using System.Text.Json;
+using FsCheck;
+using FsCheck.Xunit;
using MediaBrowser.Common.Json.Converters;
using Xunit;
namespace Jellyfin.Common.Tests.Json
{
- public static class JsonBoolNumberTests
+ public class JsonBoolNumberTests
{
+ private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
+ {
+ Converters =
+ {
+ new JsonBoolNumberConverter()
+ }
+ };
+
[Theory]
[InlineData("1", true)]
[InlineData("0", false)]
[InlineData("2", true)]
[InlineData("true", true)]
[InlineData("false", false)]
- public static void Deserialize_Number_Valid_Success(string input, bool? output)
+ public void Deserialize_Number_Valid_Success(string input, bool? output)
{
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonBoolNumberConverter());
- var value = JsonSerializer.Deserialize<bool>(input, options);
+ var value = JsonSerializer.Deserialize<bool>(input, _jsonOptions);
Assert.Equal(value, output);
}
[Theory]
[InlineData(true, "true")]
[InlineData(false, "false")]
- public static void Serialize_Bool_Success(bool input, string output)
+ public void Serialize_Bool_Success(bool input, string output)
{
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonBoolNumberConverter());
- var value = JsonSerializer.Serialize(input, options);
+ var value = JsonSerializer.Serialize(input, _jsonOptions);
Assert.Equal(value, output);
}
+
+ [Property]
+ public Property Deserialize_NonZeroInt_True(NonZeroInt input)
+ => JsonSerializer.Deserialize<bool>(input.ToString(), _jsonOptions).ToProperty();
}
-} \ No newline at end of file
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
index 0d2bdd1af..ca300401d 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
@@ -1,4 +1,5 @@
-using System.Text.Json;
+using System;
+using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Common.Tests.Models;
using MediaBrowser.Model.Session;
@@ -9,6 +10,27 @@ namespace Jellyfin.Common.Tests.Json
public static class JsonCommaDelimitedArrayTests
{
[Fact]
+ public static void Deserialize_String_Null_Success()
+ {
+ var options = new JsonSerializerOptions();
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", options);
+ Assert.Null(value?.Value);
+ }
+
+ [Fact]
+ public static 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);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
public static void Deserialize_String_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
@@ -49,6 +71,34 @@ namespace Jellyfin.Common.Tests.Json
}
[Fact]
+ public static 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);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
+ public static 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);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
public static void Deserialize_GenericCommandType_Space_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
index 1e1cde957..dbfad3c2f 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Globalization;
using System.Text.Json;
using MediaBrowser.Common.Json.Converters;
using Xunit;
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
index 22bc7afb9..cb3b66c4c 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Globalization;
using System.Text.Json;
using MediaBrowser.Common.Json.Converters;
using Xunit;
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs
index faed086a1..efe8063a0 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs
@@ -39,6 +39,15 @@ namespace Jellyfin.Common.Tests.Json
}
[Theory]
+ [InlineData("\"8\"", 8)]
+ [InlineData("8", 8)]
+ public void Deserialize_NullableInt_Success(string input, int? expected)
+ {
+ var result = JsonSerializer.Deserialize<int?>(input, _options);
+ Assert.Equal(result, expected);
+ }
+
+ [Theory]
[InlineData("\"N/A\"")]
[InlineData("null")]
public void Deserialization_To_Nullable_String_Shoud_Be_Null(string input)
@@ -48,21 +57,11 @@ namespace Jellyfin.Common.Tests.Json
}
[Theory]
- [InlineData("\"8\"", 8)]
- [InlineData("8", 8)]
- public void Deserialize_Int_Success(string input, int expected)
- {
- var result = JsonSerializer.Deserialize<int>(input, _options);
- Assert.Equal(result, expected);
- }
-
- [Fact]
- public void Deserialize_Normal_String_Success()
+ [InlineData("\"Jellyfin\"", "Jellyfin")]
+ public void Deserialize_Normal_String_Success(string input, string expected)
{
- const string Input = "\"Jellyfin\"";
- const string Expected = "Jellyfin";
- var result = JsonSerializer.Deserialize<string>(Input, _options);
- Assert.Equal(Expected, result);
+ var result = JsonSerializer.Deserialize<string?>(input, _options);
+ Assert.Equal(expected, result);
}
[Fact]
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
new file mode 100644
index 000000000..2b23c6705
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
@@ -0,0 +1,38 @@
+using System.Text.Json;
+using MediaBrowser.Common.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public class JsonStringConverterTests
+ {
+ private readonly JsonSerializerOptions _jsonSerializerOptions = new ()
+ {
+ Converters =
+ {
+ new JsonStringConverter()
+ }
+ };
+
+ [Theory]
+ [InlineData("\"test\"", "test")]
+ [InlineData("123", "123")]
+ [InlineData("123.45", "123.45")]
+ [InlineData("true", "true")]
+ [InlineData("false", "false")]
+ public void Deserialize_String_Valid_Success(string input, string output)
+ {
+ var deserialized = JsonSerializer.Deserialize<string>(input, _jsonSerializerOptions);
+ Assert.Equal(deserialized, output);
+ }
+
+ [Fact]
+ public void Deserialize_Int32asInt32_Valid_Success()
+ {
+ const string? input = "123";
+ const int output = 123;
+ var deserialized = JsonSerializer.Deserialize<int>(input, _jsonSerializerOptions);
+ Assert.Equal(deserialized, output);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs
new file mode 100644
index 000000000..f2cefdbf8
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Text.Json;
+using MediaBrowser.Common.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Json
+{
+ public class JsonVersionConverterTests
+ {
+ private readonly JsonSerializerOptions _options;
+
+ public JsonVersionConverterTests()
+ {
+ _options = new JsonSerializerOptions();
+ _options.Converters.Add(new JsonVersionConverter());
+ }
+
+ [Fact]
+ public void Deserialize_Version_Success()
+ {
+ var input = "\"1.025.222\"";
+ var output = new Version(1, 25, 222);
+ var deserializedInput = JsonSerializer.Deserialize<Version>(input, _options);
+ Assert.Equal(output, deserializedInput);
+ }
+
+ [Fact]
+ public void Serialize_Version_Success()
+ {
+ var input = new Version(1, 09, 59);
+ var output = "\"1.9.59\"";
+ var serializedInput = JsonSerializer.Serialize(input, _options);
+ Assert.Equal(output, serializedInput);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs
deleted file mode 100644
index c4422bd10..000000000
--- a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Cryptography;
-using Xunit;
-
-namespace Jellyfin.Common.Tests
-{
- public class PasswordHashTests
- {
- [Theory]
- [InlineData(
- "$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
- "PBKDF2",
- "",
- "62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
- public void ParseTest(string passwordHash, string id, string salt, string hash)
- {
- var pass = PasswordHash.Parse(passwordHash);
- Assert.Equal(id, pass.Id);
- Assert.Equal(salt, Convert.ToHexString(pass.Salt));
- Assert.Equal(hash, Convert.ToHexString(pass.Hash));
- }
-
- [Theory]
- [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
- public void ToStringTest(string passwordHash)
- {
- Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString());
- }
- }
-}
diff --git a/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs b/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs
new file mode 100644
index 000000000..ef9d31cc1
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs
@@ -0,0 +1,85 @@
+using System;
+using MediaBrowser.Common.Providers;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Providers
+{
+ public class ProviderIdParserTests
+ {
+ [Theory]
+ [InlineData("tt1234567", "tt1234567")]
+ [InlineData("tt12345678", "tt12345678")]
+ [InlineData("https://www.imdb.com/title/tt1234567", "tt1234567")]
+ [InlineData("https://www.imdb.com/title/tt12345678", "tt12345678")]
+ [InlineData(@"multiline\nhttps://www.imdb.com/title/tt1234567", "tt1234567")]
+ [InlineData(@"multiline\nhttps://www.imdb.com/title/tt12345678", "tt12345678")]
+ [InlineData("tt1234567tt7654321", "tt1234567")]
+ [InlineData("tt12345678tt7654321", "tt12345678")]
+ [InlineData("tt123456789", "tt12345678")]
+ public void FindImdbId_Valid_Success(string text, string expected)
+ {
+ Assert.True(ProviderIdParsers.TryFindImdbId(text, out ReadOnlySpan<char> parsedId));
+ Assert.Equal(expected, parsedId.ToString());
+ }
+
+ [Theory]
+ [InlineData("tt123456")]
+ [InlineData("https://www.imdb.com/title/tt123456")]
+ [InlineData("Jellyfin")]
+ public void FindImdbId_Invalid_Success(string text)
+ {
+ Assert.False(ProviderIdParsers.TryFindImdbId(text, out _));
+ }
+
+ [Theory]
+ [InlineData("https://www.themoviedb.org/movie/30287-fallo", "30287")]
+ [InlineData("themoviedb.org/movie/30287", "30287")]
+ public void FindTmdbMovieId_Valid_Success(string text, string expected)
+ {
+ Assert.True(ProviderIdParsers.TryFindTmdbMovieId(text, out ReadOnlySpan<char> parsedId));
+ Assert.Equal(expected, parsedId.ToString());
+ }
+
+ [Theory]
+ [InlineData("https://www.themoviedb.org/movie/fallo-30287")]
+ [InlineData("https://www.themoviedb.org/tv/1668-friends")]
+ public void FindTmdbMovieId_Invalid_Success(string text)
+ {
+ Assert.False(ProviderIdParsers.TryFindTmdbMovieId(text, out _));
+ }
+
+ [Theory]
+ [InlineData("https://www.themoviedb.org/tv/1668-friends", "1668")]
+ [InlineData("themoviedb.org/tv/1668", "1668")]
+ public void FindTmdbSeriesId_Valid_Success(string text, string expected)
+ {
+ Assert.True(ProviderIdParsers.TryFindTmdbSeriesId(text, out ReadOnlySpan<char> parsedId));
+ Assert.Equal(expected, parsedId.ToString());
+ }
+
+ [Theory]
+ [InlineData("https://www.themoviedb.org/tv/friends-1668")]
+ [InlineData("https://www.themoviedb.org/movie/30287-fallo")]
+ public void FindTmdbSeriesId_Invalid_Success(string text)
+ {
+ Assert.False(ProviderIdParsers.TryFindTmdbSeriesId(text, out _));
+ }
+
+ [Theory]
+ [InlineData("https://www.thetvdb.com/?tab=series&id=121361", "121361")]
+ [InlineData("thetvdb.com/?tab=series&id=121361", "121361")]
+ public void FindTvdbId_Valid_Success(string text, string expected)
+ {
+ Assert.True(ProviderIdParsers.TryFindTvdbId(text, out ReadOnlySpan<char> parsedId));
+ Assert.Equal(expected, parsedId.ToString());
+ }
+
+ [Theory]
+ [InlineData("thetvdb.com/?tab=series&id=Jellyfin121361")]
+ [InlineData("https://www.themoviedb.org/tv/1668-friends")]
+ public void FindTvdbId_Invalid_Success(string text)
+ {
+ Assert.False(ProviderIdParsers.TryFindTvdbId(text, out _));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs
new file mode 100644
index 000000000..feffb50e8
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs
@@ -0,0 +1,200 @@
+using System.Linq;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Controller.Tests
+{
+ public class DirectoryServiceTests
+ {
+ private const string LowerCasePath = "/music/someartist";
+ private const string UpperCasePath = "/music/SOMEARTIST";
+
+ private static readonly FileSystemMetadata[] _lowerCaseFileSystemMetadata =
+ {
+ new ()
+ {
+ FullName = LowerCasePath + "/Artwork",
+ IsDirectory = true
+ },
+ new ()
+ {
+ FullName = LowerCasePath + "/Some Other Folder",
+ IsDirectory = true
+ },
+ new ()
+ {
+ FullName = LowerCasePath + "/Song 2.mp3",
+ IsDirectory = false
+ },
+ new ()
+ {
+ FullName = LowerCasePath + "/Song 3.mp3",
+ IsDirectory = false
+ }
+ };
+
+ private static readonly FileSystemMetadata[] _upperCaseFileSystemMetadata =
+ {
+ new ()
+ {
+ FullName = UpperCasePath + "/Lyrics",
+ IsDirectory = true
+ },
+ new ()
+ {
+ FullName = UpperCasePath + "/Song 1.mp3",
+ IsDirectory = false
+ }
+ };
+
+ [Fact]
+ public void GetFileSystemEntries_GivenPathsWithDifferentCasing_CachesAll()
+ {
+ var fileSystemMock = new Mock<IFileSystem>();
+ fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata);
+ fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata);
+ var directoryService = new DirectoryService(fileSystemMock.Object);
+
+ var upperCaseResult = directoryService.GetFileSystemEntries(UpperCasePath);
+ var lowerCaseResult = directoryService.GetFileSystemEntries(LowerCasePath);
+
+ Assert.Equal(_upperCaseFileSystemMetadata, upperCaseResult);
+ Assert.Equal(_lowerCaseFileSystemMetadata, lowerCaseResult);
+ }
+
+ [Fact]
+ public void GetFiles_GivenPathsWithDifferentCasing_ReturnsCorrectFiles()
+ {
+ var fileSystemMock = new Mock<IFileSystem>();
+ fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata);
+ fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata);
+ var directoryService = new DirectoryService(fileSystemMock.Object);
+
+ var upperCaseResult = directoryService.GetFiles(UpperCasePath);
+ var lowerCaseResult = directoryService.GetFiles(LowerCasePath);
+
+ Assert.Equal(_upperCaseFileSystemMetadata.Where(f => !f.IsDirectory), upperCaseResult);
+ Assert.Equal(_lowerCaseFileSystemMetadata.Where(f => !f.IsDirectory), lowerCaseResult);
+ }
+
+ [Fact]
+ public void GetFile_GivenFilePathsWithDifferentCasing_ReturnsCorrectFile()
+ {
+ const string lowerCasePath = "/music/someartist/song 1.mp3";
+ var lowerCaseFileSystemMetadata = new FileSystemMetadata
+ {
+ FullName = lowerCasePath,
+ Exists = true
+ };
+ const string upperCasePath = "/music/SOMEARTIST/SONG 1.mp3";
+ var upperCaseFileSystemMetadata = new FileSystemMetadata
+ {
+ FullName = upperCasePath,
+ Exists = false
+ };
+ var fileSystemMock = new Mock<IFileSystem>();
+ fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata);
+ fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata);
+ var directoryService = new DirectoryService(fileSystemMock.Object);
+
+ var lowerCaseResult = directoryService.GetFile(lowerCasePath);
+ var upperCaseResult = directoryService.GetFile(upperCasePath);
+
+ Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseResult);
+ Assert.Null(upperCaseResult);
+ }
+
+ [Fact]
+ public void GetFile_GivenCachedPath_ReturnsCachedFile()
+ {
+ const string path = "/music/someartist/song 1.mp3";
+ var cachedFileSystemMetadata = new FileSystemMetadata
+ {
+ FullName = path,
+ Exists = true
+ };
+ var newFileSystemMetadata = new FileSystemMetadata
+ {
+ FullName = "/music/SOMEARTIST/song 1.mp3",
+ Exists = true
+ };
+
+ var fileSystemMock = new Mock<IFileSystem>();
+ fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == path))).Returns(cachedFileSystemMetadata);
+ var directoryService = new DirectoryService(fileSystemMock.Object);
+
+ var result = directoryService.GetFile(path);
+ fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata);
+ var secondResult = directoryService.GetFile(path);
+
+ Assert.Equal(cachedFileSystemMetadata, result);
+ Assert.Equal(cachedFileSystemMetadata, secondResult);
+ }
+
+ [Fact]
+ public void GetFilePaths_GivenCachedFilePathWithoutClear_ReturnsOnlyCachedPaths()
+ {
+ const string path = "/music/someartist";
+
+ var cachedPaths = new[]
+ {
+ "/music/someartist/song 1.mp3",
+ "/music/someartist/song 2.mp3",
+ "/music/someartist/song 3.mp3",
+ "/music/someartist/song 4.mp3",
+ };
+ var newPaths = new[]
+ {
+ "/music/someartist/song 5.mp3",
+ "/music/someartist/song 6.mp3",
+ "/music/someartist/song 7.mp3",
+ "/music/someartist/song 8.mp3",
+ };
+
+ var fileSystemMock = new Mock<IFileSystem>();
+ fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
+ var directoryService = new DirectoryService(fileSystemMock.Object);
+
+ var result = directoryService.GetFilePaths(path);
+ fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
+ var secondResult = directoryService.GetFilePaths(path);
+
+ Assert.Equal(cachedPaths, result);
+ Assert.Equal(cachedPaths, secondResult);
+ }
+
+ [Fact]
+ public void GetFilePaths_GivenCachedFilePathWithClear_ReturnsNewPaths()
+ {
+ const string path = "/music/someartist";
+
+ var cachedPaths = new[]
+ {
+ "/music/someartist/song 1.mp3",
+ "/music/someartist/song 2.mp3",
+ "/music/someartist/song 3.mp3",
+ "/music/someartist/song 4.mp3",
+ };
+ var newPaths = new[]
+ {
+ "/music/someartist/song 5.mp3",
+ "/music/someartist/song 6.mp3",
+ "/music/someartist/song 7.mp3",
+ "/music/someartist/song 8.mp3",
+ };
+
+ var fileSystemMock = new Mock<IFileSystem>();
+ fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
+ var directoryService = new DirectoryService(fileSystemMock.Object);
+
+ var result = directoryService.GetFilePaths(path);
+ fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
+ var secondResult = directoryService.GetFilePaths(path, true);
+
+ Assert.Equal(cachedPaths, result);
+ Assert.Equal(newPaths, secondResult);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs b/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs
new file mode 100644
index 000000000..576c0a49b
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs
@@ -0,0 +1,19 @@
+using System;
+using MediaBrowser.Controller.Extensions;
+using Xunit;
+
+namespace Jellyfin.Controller.Extensions.Tests
+{
+ public class StringExtensionsTests
+ {
+ [Theory]
+ [InlineData("", '_', 0)]
+ [InlineData("___", '_', 3)]
+ [InlineData("test\x00", '\x00', 1)]
+ [InlineData("Imdb=tt0119567|Tmdb=330|TmdbCollection=328", '|', 2)]
+ public void ReadOnlySpan_Count_Success(string str, char needle, int count)
+ {
+ Assert.Equal(count, str.AsSpan().Count(needle));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index c766c5445..9a8ddafa0 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -10,18 +10,20 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -31,8 +33,4 @@
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs b/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs
new file mode 100644
index 000000000..668bd8f87
--- /dev/null
+++ b/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs
@@ -0,0 +1,131 @@
+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/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 52a9e1193..1f6cd541c 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -5,18 +5,20 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -26,8 +28,4 @@
<ProjectReference Include="../../Emby.Dlna/Emby.Dlna.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
index c39ef0ce9..415682e85 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
@@ -16,7 +16,7 @@ namespace Jellyfin.MediaEncoding.Tests
var path = Path.Join("Test Data", fileName);
using (var stream = File.OpenRead(path))
{
- await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.GetOptions()).ConfigureAwait(false);
+ await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 24f6fb356..6b828e113 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -10,6 +10,8 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -19,15 +21,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -37,8 +38,4 @@
<ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
new file mode 100644
index 000000000..98fbb00d5
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text.Json;
+using MediaBrowser.Common.Json;
+using MediaBrowser.MediaEncoding.Probing;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Tests.Probing
+{
+ public class ProbeResultNormalizerTests
+ {
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null);
+
+ [Fact]
+ public void GetMediaInfo_MetaData_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_metadata.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File);
+
+ Assert.Single(res.MediaStreams);
+
+ Assert.NotNull(res.VideoStream);
+ Assert.Equal("4:3", res.VideoStream.AspectRatio);
+ Assert.Equal(25f, res.VideoStream.AverageFrameRate);
+ Assert.Equal(8, res.VideoStream.BitDepth);
+ Assert.Equal(69432, res.VideoStream.BitRate);
+ Assert.Equal("h264", res.VideoStream.Codec);
+ Assert.Equal("1/50", res.VideoStream.CodecTimeBase);
+ Assert.Equal(240, res.VideoStream.Height);
+ Assert.Equal(320, res.VideoStream.Width);
+ Assert.Equal(0, res.VideoStream.Index);
+ Assert.False(res.VideoStream.IsAnamorphic);
+ Assert.True(res.VideoStream.IsAVC);
+ Assert.True(res.VideoStream.IsDefault);
+ Assert.False(res.VideoStream.IsExternal);
+ Assert.False(res.VideoStream.IsForced);
+ Assert.False(res.VideoStream.IsInterlaced);
+ Assert.False(res.VideoStream.IsTextSubtitleStream);
+ Assert.Equal(13d, res.VideoStream.Level);
+ Assert.Equal("4", res.VideoStream.NalLengthSize);
+ Assert.Equal("yuv444p", res.VideoStream.PixelFormat);
+ Assert.Equal("High 4:4:4 Predictive", res.VideoStream.Profile);
+ Assert.Equal(25f, res.VideoStream.RealFrameRate);
+ Assert.Equal(1, res.VideoStream.RefFrames);
+ Assert.Equal("1/1000", res.VideoStream.TimeBase);
+ Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
+
+ Assert.Empty(res.Chapters);
+ Assert.Equal("Just color bars", res.Overview);
+ }
+
+ [Fact]
+ public void GetMediaInfo_MusicVideo_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/music_video_metadata.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/music_video.mkv", MediaProtocol.File);
+
+ Assert.Equal("The Title", res.Name);
+ Assert.Equal("Title, The", res.ForcedSortName);
+ Assert.Single(res.Artists);
+ Assert.Equal("The Artist", res.Artists[0]);
+ Assert.Equal("Album", res.Album);
+ Assert.Equal(2021, res.ProductionYear);
+ Assert.True(res.PremiereDate.HasValue);
+ Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/SsaParserTests.cs
deleted file mode 100644
index d11cb242c..000000000
--- a/tests/Jellyfin.MediaEncoding.Tests/SsaParserTests.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading;
-using MediaBrowser.MediaEncoding.Subtitles;
-using MediaBrowser.Model.MediaInfo;
-using Xunit;
-
-namespace Jellyfin.MediaEncoding.Tests
-{
- public class SsaParserTests
- {
- // commonly shared invariant value between tests, assumes default format order
- private const string InvariantDialoguePrefix = "[Events]\nDialogue: ,0:00:00.00,0:00:00.01,,,,,,,";
-
- private SsaParser parser = new SsaParser();
-
- [Theory]
- [InlineData("[EvEnTs]\nDialogue: ,0:00:00.00,0:00:00.01,,,,,,,text", "text")] // label casing insensitivity
- [InlineData("[Events]\n,0:00:00.00,0:00:00.01,,,,,,,labelless dialogue", "labelless dialogue")] // no "Dialogue:" label, it is optional
- [InlineData("[Events]\nFormat: Text, Start, End, Layer, Effect, Style\nDialogue: reordered text,0:00:00.00,0:00:00.01", "reordered text")] // reordered formats
- [InlineData(InvariantDialoguePrefix + "Cased TEXT", "Cased TEXT")] // preserve text casing
- [InlineData(InvariantDialoguePrefix + " text ", " text ")] // do not trim text
- [InlineData(InvariantDialoguePrefix + "text, more text", "text, more text")] // append excess dialogue values (> 10) to text
- [InlineData(InvariantDialoguePrefix + "start {\\fnFont Name}text{\\fn} end", "start <font face=\"Font Name\">text</font> end")] // font name
- [InlineData(InvariantDialoguePrefix + "start {\\fs10}text{\\fs} end", "start <font size=\"10\">text</font> end")] // font size
- [InlineData(InvariantDialoguePrefix + "start {\\c&H112233}text{\\c} end", "start <font color=\"#332211\">text</font> end")] // color
- [InlineData(InvariantDialoguePrefix + "start {\\1c&H112233}text{\\1c} end", "start <font color=\"#332211\">text</font> end")] // primay color
- [InlineData(InvariantDialoguePrefix + "start {\\fnFont Name}text1 {\\fs10}text2{\\fs}{\\fn} {\\1c&H112233}text3{\\1c} end", "start <font face=\"Font Name\">text1 <font size=\"10\">text2</font></font> <font color=\"#332211\">text3</font> end")] // nested formatting
- public void Parse(string ssa, string expectedText)
- {
- using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
- {
- SubtitleTrackInfo subtitleTrackInfo = parser.Parse(stream, CancellationToken.None);
- SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[0];
- Assert.Equal(expectedText, actual.Text);
- }
- }
-
- [Theory]
- [MemberData(nameof(Parse_MultipleDialogues_TestData))]
- public void Parse_MultipleDialogues(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents)
- {
- using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
- {
- SubtitleTrackInfo subtitleTrackInfo = parser.Parse(stream, CancellationToken.None);
-
- Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
-
- for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i)
- {
- SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i];
- SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i];
-
- Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks);
- Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks);
- Assert.Equal(expected.Text, actual.Text);
- }
- }
- }
-
- public static IEnumerable<object[]> Parse_MultipleDialogues_TestData()
- {
- yield return new object[]
- {
- @"[Events]
- Format: Layer, Start, End, Text
- Dialogue: ,0:00:01.18,0:00:01.85,dialogue1
- Dialogue: ,0:00:02.18,0:00:02.85,dialogue2
- Dialogue: ,0:00:03.18,0:00:03.85,dialogue3
- ",
- new List<SubtitleTrackEvent>
- {
- new SubtitleTrackEvent
- {
- StartPositionTicks = 11800000,
- EndPositionTicks = 18500000,
- Text = "dialogue1"
- },
- new SubtitleTrackEvent
- {
- StartPositionTicks = 21800000,
- EndPositionTicks = 28500000,
- Text = "dialogue2"
- },
- new SubtitleTrackEvent
- {
- StartPositionTicks = 31800000,
- EndPositionTicks = 38500000,
- Text = "dialogue3"
- }
- }
- };
- }
- }
-}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
index 14ad49839..3775555de 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
@@ -3,6 +3,7 @@ using System.Globalization;
using System.IO;
using System.Threading;
using MediaBrowser.MediaEncoding.Subtitles;
+using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Jellyfin.MediaEncoding.Subtitles.Tests
@@ -14,25 +15,15 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
using (var stream = File.OpenRead("Test Data/example.ass"))
{
- var parsed = new AssParser().Parse(stream, CancellationToken.None);
+ var parsed = new AssParser(new NullLogger<AssParser>()).Parse(stream, CancellationToken.None);
Assert.Single(parsed.TrackEvents);
var trackEvent = parsed.TrackEvents[0];
Assert.Equal("1", trackEvent.Id);
Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
- Assert.Equal("Like an Angel with pity on nobody\r\nThe second line in subtitle", trackEvent.Text);
+ Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text);
}
}
-
- [Fact]
- public void ParseFieldHeaders_Valid_Success()
- {
- const string Line = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text";
- var headers = AssParser.ParseFieldHeaders(Line);
- Assert.Equal(1, headers["Start"]);
- Assert.Equal(2, headers["End"]);
- Assert.Equal(9, headers["Text"]);
- }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
index 3e2d2de10..537a944b0 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -22,7 +22,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Assert.Equal("1", trackEvent1.Id);
Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
- Assert.Equal("Senator, we're making\r\nour final approach into Coruscant.", trackEvent1.Text);
+ Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text);
var trackEvent2 = parsed.TrackEvents[1];
Assert.Equal("2", trackEvent2.Id);
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
new file mode 100644
index 000000000..5db80c300
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading;
+using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Subtitles.Tests
+{
+ public class SsaParserTests
+ {
+ private readonly SsaParser _parser = new SsaParser(new NullLogger<AssParser>());
+
+ [Theory]
+ [MemberData(nameof(Parse_MultipleDialogues_TestData))]
+ public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents)
+ {
+ using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
+ {
+ SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, CancellationToken.None);
+
+ Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
+
+ for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i)
+ {
+ SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i];
+ SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i];
+
+ Assert.Equal(expected.Id, actual.Id);
+ Assert.Equal(expected.Text, actual.Text);
+ Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks);
+ Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks);
+ }
+ }
+ }
+
+ public static IEnumerable<object[]> Parse_MultipleDialogues_TestData()
+ {
+ yield return new object[]
+ {
+ @"[Events]
+ Format: Layer, Start, End, Text
+ Dialogue: ,0:00:01.18,0:00:01.85,dialogue1
+ Dialogue: ,0:00:02.18,0:00:02.85,dialogue2
+ Dialogue: ,0:00:03.18,0:00:03.85,dialogue3
+ ",
+ new List<SubtitleTrackEvent>
+ {
+ new SubtitleTrackEvent("1", "dialogue1")
+ {
+ StartPositionTicks = 11800000,
+ EndPositionTicks = 18500000
+ },
+ new SubtitleTrackEvent("2", "dialogue2")
+ {
+ StartPositionTicks = 21800000,
+ EndPositionTicks = 28500000
+ },
+ new SubtitleTrackEvent("3", "dialogue3")
+ {
+ StartPositionTicks = 31800000,
+ EndPositionTicks = 38500000
+ }
+ }
+ };
+ }
+
+ [Fact]
+ public void Parse_Valid_Success()
+ {
+ using (var stream = File.OpenRead("Test Data/example.ssa"))
+ {
+ var parsed = _parser.Parse(stream, CancellationToken.None);
+ Assert.Single(parsed.TrackEvents);
+ var trackEvent = parsed.TrackEvents[0];
+
+ Assert.Equal("1", trackEvent.Id);
+ Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
+ Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json
new file mode 100644
index 000000000..97d6600a4
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json
@@ -0,0 +1,111 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "High",
+ "codec_type": "video",
+ "codec_time_base": "1001/48000",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "width": 1920,
+ "height": 1080,
+ "coded_width": 1920,
+ "coded_height": 1088,
+ "closed_captions": 0,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "16:9",
+ "pix_fmt": "yuv420p",
+ "level": 42,
+ "chroma_location": "left",
+ "field_order": "progressive",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "r_frame_rate": "24000/1001",
+ "avg_frame_rate": "24000/1001",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "bits_per_raw_sample": "8",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_time_base": "1/48000",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "sample_fmt": "fltp",
+ "sample_rate": "48000",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ }
+ ],
+ "chapters": [
+ ],
+ "format": {
+ "filename": "music_video.mkv",
+ "nb_streams": 2,
+ "nb_programs": 0,
+ "format_name": "matroska,webm",
+ "format_long_name": "Matroska / WebM",
+ "start_time": "0.000000",
+ "duration": "180.000000",
+ "size": "500000000",
+ "bit_rate": "22222222",
+ "probe_score": 100,
+ "tags": {
+ "TITLE-eng": "The Title",
+ "TITLESORT": "Title, The",
+ "ARTIST": "The Artist",
+ "ARTISTSORT": "Artist, The",
+ "ALBUM": "Album",
+ "DATE_RELEASED": "2021-01-01"
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json
new file mode 100644
index 000000000..720fc5c8f
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json
@@ -0,0 +1,74 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "High 4:4:4 Predictive",
+ "codec_type": "video",
+ "codec_time_base": "1/50",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "width": 320,
+ "height": 240,
+ "coded_width": 320,
+ "coded_height": 240,
+ "closed_captions": 0,
+ "has_b_frames": 2,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "4:3",
+ "pix_fmt": "yuv444p",
+ "level": 13,
+ "chroma_location": "left",
+ "field_order": "progressive",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "r_frame_rate": "25/1",
+ "avg_frame_rate": "25/1",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "bits_per_raw_sample": "8",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "ENCODER": "Lavc57.107.100 libx264",
+ "DURATION": "00:00:01.000000000"
+ }
+ }
+ ],
+ "chapters": [
+
+ ],
+ "format": {
+ "filename": "some_metadata.mkv",
+ "nb_streams": 1,
+ "nb_programs": 0,
+ "format_name": "matroska,webm",
+ "format_long_name": "Matroska / WebM",
+ "start_time": "0.000000",
+ "duration": "1.000000",
+ "size": "8679",
+ "bit_rate": "69432",
+ "probe_score": 100,
+ "tags": {
+ "DESCRIPTION": "Just color bars",
+ "ARCHIVAL": "yes",
+ "PRESERVE_THIS": "okay",
+ "ENCODER": "Lavf57.83.100"
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.ssa b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.ssa
new file mode 100644
index 000000000..dcbb972eb
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.ssa
@@ -0,0 +1,20 @@
+[Script Info]
+; This is a Sub Station Alpha v4 script.
+; For Sub Station Alpha info and downloads,
+; go to http://www.eswat.demon.co.uk/
+Title: Neon Genesis Evangelion - Episode 26 (neutral Spanish)
+Original Script: RoRo
+Script Updated By: version 2.8.01
+ScriptType: v4.00
+Collisions: Normal
+PlayResY: 600
+PlayDepth: 0
+Timer: 100,0000
+
+[V4 Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding
+Style: DefaultVCD, Arial,28,11861244,11861244,11861244,-2147483640,-1,0,1,1,2,2,30,30,30,0,0
+
+[Events]
+Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+Dialogue: Marked=0,0:00:01.18,0:00:06.85,DefaultVCD, NTP,0000,0000,0000,,{\pos(400,570)}Like an angel with pity on nobody
diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs
new file mode 100644
index 000000000..cca056c28
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Model.Dlna;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Dlna
+{
+ public class ContainerProfileTests
+ {
+ private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile();
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("mp4")]
+ public void ContainsContainer_EmptyContainerProfile_True(string? containers)
+ {
+ Assert.True(_emptyContainerProfile.ContainsContainer(containers));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs
new file mode 100644
index 000000000..955d296cc
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs
@@ -0,0 +1,70 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Entities
+{
+ public class JsonLowerCaseConverterTests
+ {
+ private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
+ {
+ Converters =
+ {
+ new JsonStringEnumConverter()
+ }
+ };
+
+ [Theory]
+ [InlineData(null, "{\"CollectionType\":null}")]
+ [InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")]
+ [InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")]
+ public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected)
+ {
+ Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions));
+ }
+
+ [Theory]
+ [InlineData("{\"CollectionType\":null}", null)]
+ [InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)]
+ [InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)]
+ public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result)
+ {
+ var res = JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions);
+ Assert.NotNull(res);
+ Assert.Equal(result, res!.CollectionType);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData(CollectionTypeOptions.Movies)]
+ [InlineData(CollectionTypeOptions.MusicVideos)]
+ public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value)
+ {
+ var res = JsonSerializer.Deserialize<TestContainer>(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions);
+ Assert.NotNull(res);
+ Assert.Equal(value, res!.CollectionType);
+ }
+
+ [Theory]
+ [InlineData("{\"CollectionType\":null}")]
+ [InlineData("{\"CollectionType\":\"movies\"}")]
+ [InlineData("{\"CollectionType\":\"musicvideos\"}")]
+ public void RoundTrip_String_Correct(string json)
+ {
+ var res = JsonSerializer.Serialize(JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions), _jsonOptions);
+ Assert.Equal(json, res);
+ }
+
+ private class TestContainer
+ {
+ public TestContainer(CollectionTypeOptions? collectionType)
+ {
+ CollectionType = collectionType;
+ }
+
+ [JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))]
+ public CollectionTypeOptions? CollectionType { get; set; }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs
new file mode 100644
index 000000000..a1ace8476
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Entities
+{
+ public class ProviderIdsExtensionsTests
+ {
+ private const string ExampleImdbId = "tt0113375";
+
+ [Fact]
+ public void HasProviderId_NullInstance_ThrowsArgumentNullException()
+ {
+ Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.HasProviderId(null!, MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void HasProviderId_NullProvider_False()
+ {
+ var nullProvider = new ProviderIdsExtensionsTestsObject
+ {
+ ProviderIds = null!
+ };
+
+ Assert.False(nullProvider.HasProviderId(MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void HasProviderId_NullName_ThrowsArgumentNullException()
+ {
+ Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensionsTestsObject.Empty.HasProviderId(null!));
+ }
+
+ [Fact]
+ public void HasProviderId_NotFoundName_False()
+ {
+ Assert.False(ProviderIdsExtensionsTestsObject.Empty.HasProviderId(MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void HasProviderId_FoundName_True()
+ {
+ var provider = new ProviderIdsExtensionsTestsObject();
+ provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
+
+ Assert.True(provider.HasProviderId(MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void HasProviderId_FoundNameEmptyValue_False()
+ {
+ var provider = new ProviderIdsExtensionsTestsObject();
+ provider.ProviderIds[MetadataProvider.Imdb.ToString()] = string.Empty;
+
+ Assert.False(provider.HasProviderId(MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void GetProviderId_NullInstance_ThrowsArgumentNullException()
+ {
+ Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.GetProviderId(null!, MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void GetProviderId_NullName_ThrowsArgumentNullException()
+ {
+ Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensionsTestsObject.Empty.GetProviderId(null!));
+ }
+
+ [Fact]
+ public void GetProviderId_NotFoundName_Null()
+ {
+ Assert.Null(ProviderIdsExtensionsTestsObject.Empty.GetProviderId(MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void GetProviderId_NullProvider_Null()
+ {
+ var nullProvider = new ProviderIdsExtensionsTestsObject
+ {
+ ProviderIds = null!
+ };
+
+ Assert.Null(nullProvider.GetProviderId(MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void TryGetProviderId_NotFoundName_False()
+ {
+ Assert.False(ProviderIdsExtensionsTestsObject.Empty.TryGetProviderId(MetadataProvider.Imdb, out _));
+ }
+
+ [Fact]
+ public void TryGetProviderId_NullProvider_False()
+ {
+ var nullProvider = new ProviderIdsExtensionsTestsObject
+ {
+ ProviderIds = null!
+ };
+
+ Assert.False(nullProvider.TryGetProviderId(MetadataProvider.Imdb, out _));
+ }
+
+ [Fact]
+ public void GetProviderId_FoundName_Id()
+ {
+ var provider = new ProviderIdsExtensionsTestsObject();
+ provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
+
+ Assert.Equal(ExampleImdbId, provider.GetProviderId(MetadataProvider.Imdb));
+ }
+
+ [Fact]
+ public void TryGetProviderId_FoundName_True()
+ {
+ var provider = new ProviderIdsExtensionsTestsObject();
+ provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
+
+ Assert.True(provider.TryGetProviderId(MetadataProvider.Imdb, out var id));
+ Assert.Equal(ExampleImdbId, id);
+ }
+
+ [Fact]
+ public void TryGetProviderId_FoundNameEmptyValue_False()
+ {
+ var provider = new ProviderIdsExtensionsTestsObject();
+ provider.ProviderIds[MetadataProvider.Imdb.ToString()] = string.Empty;
+
+ Assert.False(provider.TryGetProviderId(MetadataProvider.Imdb, out var id));
+ Assert.Null(id);
+ }
+
+ [Fact]
+ public void SetProviderId_NullInstance_ThrowsArgumentNullException()
+ {
+ Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.SetProviderId(null!, MetadataProvider.Imdb, ExampleImdbId));
+ }
+
+ [Fact]
+ public void SetProviderId_Null_Remove()
+ {
+ var provider = new ProviderIdsExtensionsTestsObject();
+ provider.SetProviderId(MetadataProvider.Imdb, null!);
+ Assert.Empty(provider.ProviderIds);
+ }
+
+ [Fact]
+ public void SetProviderId_EmptyName_Remove()
+ {
+ var provider = new ProviderIdsExtensionsTestsObject();
+ provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
+ provider.SetProviderId(MetadataProvider.Imdb, string.Empty);
+ Assert.Empty(provider.ProviderIds);
+ }
+
+ [Fact]
+ public void SetProviderId_NonEmptyId_Success()
+ {
+ var provider = new ProviderIdsExtensionsTestsObject();
+ provider.SetProviderId(MetadataProvider.Imdb, ExampleImdbId);
+ Assert.Single(provider.ProviderIds);
+ }
+
+ [Fact]
+ public void SetProviderId_NullProvider_Success()
+ {
+ var nullProvider = new ProviderIdsExtensionsTestsObject
+ {
+ ProviderIds = null!
+ };
+
+ nullProvider.SetProviderId(MetadataProvider.Imdb, ExampleImdbId);
+ Assert.Single(nullProvider.ProviderIds);
+ }
+
+ [Fact]
+ public void SetProviderId_NullProviderAndEmptyName_Success()
+ {
+ var nullProvider = new ProviderIdsExtensionsTestsObject
+ {
+ ProviderIds = null!
+ };
+
+ nullProvider.SetProviderId(MetadataProvider.Imdb, string.Empty);
+ Assert.Null(nullProvider.ProviderIds);
+ }
+
+ private class ProviderIdsExtensionsTestsObject : IHasProviderIds
+ {
+ public static readonly ProviderIdsExtensionsTestsObject Empty = new ProviderIdsExtensionsTestsObject();
+
+ public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
+ }
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
index 51633e157..0a4e060df 100644
--- a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
+++ b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
@@ -1,4 +1,6 @@
using System;
+using FsCheck;
+using FsCheck.Xunit;
using MediaBrowser.Model.Extensions;
using Xunit;
@@ -11,9 +13,20 @@ namespace Jellyfin.Model.Tests.Extensions
[InlineData("banana", "Banana")]
[InlineData("Banana", "Banana")]
[InlineData("ä", "Ä")]
+ [InlineData("\027", "\027")]
public void StringHelper_ValidArgs_Success(string input, string expectedResult)
{
Assert.Equal(expectedResult, StringHelper.FirstToUpper(input));
}
+
+ [Property]
+ public Property FirstToUpper_RandomArg_Correct(NonEmptyString input)
+ {
+ var result = StringHelper.FirstToUpper(input.Item);
+
+ // We check IsLower instead of IsUpper because both return false for non-letters
+ return (!char.IsLower(result[0])).Label("First char is uppercase")
+ .And(input.Item.Length == 1 || result[1..].Equals(input.Item[1..], StringComparison.Ordinal)).Label("Remaining chars are unmodified");
+ }
}
}
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 64d51e063..40c51e524 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -5,18 +5,20 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
- <PackageReference Include="coverlet.collector" Version="1.2.1" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -26,8 +28,4 @@
<ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
index e5768b620..d9e77dd2e 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index b3257ace3..53b35c2d6 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
@@ -10,7 +9,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
private readonly NamingOptions _namingOptions = new NamingOptions();
- public static IEnumerable<object[]> GetResolveFileTestData()
+ public static IEnumerable<object[]> Resolve_ValidFileNameTestData()
{
yield return new object[]
{
@@ -36,7 +35,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
}
[Theory]
- [MemberData(nameof(GetResolveFileTestData))]
+ [MemberData(nameof(Resolve_ValidFileNameTestData))]
public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult)
{
var result = new AudioBookResolver(_namingOptions).Resolve(expectedResult.Path);
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index a4d5c0d6f..e386cb8c1 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -8,15 +8,17 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
- <Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<ItemGroup>
@@ -25,14 +27,9 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
index f3abacb4f..2446660f3 100644
--- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs
@@ -1,4 +1,3 @@
-using System;
using Emby.Naming.Common;
using Emby.Naming.Subtitles;
using Xunit;
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
index 950899d7e..b1141df47 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
@@ -58,7 +58,7 @@ namespace Jellyfin.Naming.Tests.Video
{
input = Path.GetFileName(input);
- var result = new VideoResolver(_namingOptions).CleanDateTime(input);
+ var result = VideoResolver.CleanDateTime(input, _namingOptions);
Assert.Equal(expectedName, result.Name, true);
Assert.Equal(expectedYear, result.Year);
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
index fde06c5a1..fb050cf5a 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
@@ -14,11 +14,6 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Super movie 480p 2001.mp4", "Super movie")]
[InlineData("Super movie [480p].mp4", "Super movie")]
[InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")]
- [InlineData("Super movie(2009).mp4", "Super movie(2009).mp4")]
- [InlineData("Run lola run (lola rennt) (2009).mp4", "Run lola run (lola rennt) (2009).mp4")]
- [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv")]
- [InlineData(@"American Psycho.mkv", "American Psycho.mkv")]
- [InlineData(@"[rec].mkv", "[rec].mkv")]
[InlineData("Crouching.Tiger.Hidden.Dragon.4k.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.UltraHD.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.UHD.mkv", "Crouching.Tiger.Hidden.Dragon")]
@@ -29,17 +24,25 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
- public void CleanStringTest(string input, string expectedName)
+ public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
{
- if (new VideoResolver(_namingOptions).TryCleanString(input, out ReadOnlySpan<char> newName))
- {
- // TODO: compare spans when XUnit supports it
- Assert.Equal(expectedName, newName.ToString());
- }
- else
- {
- Assert.Equal(expectedName, input);
- }
+ Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
+ // TODO: compare spans when XUnit supports it
+ Assert.Equal(expectedName, newName.ToString());
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("Super movie(2009).mp4")]
+ [InlineData("[rec].mkv")]
+ [InlineData("American.Psycho.mkv")]
+ [InlineData("American Psycho.mkv")]
+ [InlineData("Run lola run (lola rennt) (2009).mp4")]
+ public void CleanStringTest_DoesntNeedCleaning_False(string? input)
+ {
+ Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
+ Assert.True(newName.IsEmpty);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index d34f65409..f872f94f8 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -1,4 +1,3 @@
-using System;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
@@ -105,13 +104,6 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(rule, res.Rule);
}
- [Fact]
- public void TestFlagsParser()
- {
- var flags = new FlagParser(_videoOptions).GetFlags(string.Empty);
- Assert.Empty(flags);
- }
-
private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
{
return new ExtraResolver(videoOptions);
diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
index 69de96a47..1762b91b9 100644
--- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
@@ -22,8 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void Test3DName()
{
- var result =
- new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
+ 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);
@@ -58,15 +57,13 @@ namespace Jellyfin.Naming.Tests.Video
private void Test(string input, bool is3D, string? format3D)
{
- var parser = new Format3DParser(_namingOptions);
-
- var result = parser.Parse(input);
+ var result = Format3DParser.Parse(input, _namingOptions);
Assert.Equal(is3D, result.Is3D);
if (format3D == null)
{
- Assert.Null(result.Format3D);
+ Assert.Null(result?.Format3D);
}
else
{
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index bc5e6fa63..d02f8ae92 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class MultiVersionTests
{
- private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions());
+ private readonly NamingOptions _namingOptions = new NamingOptions();
[Fact]
public void TestMultiEdition1()
@@ -22,11 +22,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
@@ -43,11 +45,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
@@ -63,11 +67,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -87,11 +93,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/M/Movie 7.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
@@ -113,11 +121,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Movie/Movie-8.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Mo/Movie 9.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(9, result.Count);
Assert.Empty(result[0].Extras);
@@ -163,11 +175,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Movie/Movie 5.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -188,11 +202,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man (2011).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -214,11 +230,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man[test].mkv",
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -243,11 +261,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man [test].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -266,11 +286,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man - C (2007).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -289,18 +311,17 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
- Assert.Equal(6, result[0].AlternateVersions.Count);
- Assert.False(result[0].AlternateVersions[2].Is3D);
- Assert.True(result[0].AlternateVersions[3].Is3D);
- Assert.True(result[0].AlternateVersions[4].Is3D);
+ Assert.Empty(result[0].AlternateVersions);
}
[Fact]
@@ -317,11 +338,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man (2011).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -337,11 +360,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -357,11 +382,35 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
+
+ Assert.Single(result);
+ Assert.Empty(result[0].Extras);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ [Fact]
+ public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName()
+ {
+ var files = new[]
{
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ @"/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(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -369,9 +418,29 @@ namespace Jellyfin.Naming.Tests.Video
}
[Fact]
+ public void Resolve_GivenUnclosedBrackets_DoesNotGroup()
+ {
+ 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"
+ };
+
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
+
+ Assert.Equal(2, result.Count);
+ }
+
+ [Fact]
public void TestEmptyList()
{
- var result = _videoListResolver.Resolve(new List<FileSystemMetadata>()).ToList();
+ var result = VideoListResolver.Resolve(new List<FileSystemMetadata>(), _namingOptions).ToList();
Assert.Empty(result);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
index 6e759c6d6..1d50df7a6 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
@@ -29,8 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestStubName()
{
- var result =
- new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
+ 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 215c7e540..9e0776c3c 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@@ -10,9 +11,8 @@ namespace Jellyfin.Naming.Tests.Video
{
private readonly NamingOptions _namingOptions = new NamingOptions();
- // FIXME
- // [Fact]
- private void TestStackAndExtras()
+ [Fact]
+ public void TestStackAndExtras()
{
// No stacking here because there is no part/disc/etc
var files = new[]
@@ -40,23 +40,24 @@ namespace Jellyfin.Naming.Tests.Video
"WillyWonka-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
-
- Assert.Equal(3, result[1].Files.Count);
- Assert.Equal(3, result[1].Extras.Count);
- Assert.Equal("Batman", result[1].Name);
-
- Assert.Equal(4, result[2].Files.Count);
- Assert.Equal(2, result[2].Extras.Count);
- Assert.Equal("Harry Potter and the Deathly Hallows", result[2].Name);
+ var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
+ Assert.NotNull(batman);
+ Assert.Equal(3, batman!.Files.Count);
+ Assert.Equal(3, batman!.Extras.Count);
+
+ var harry = result.FirstOrDefault(x => string.Equals(x.Name, "Harry Potter and the Deathly Hallows", StringComparison.Ordinal));
+ Assert.NotNull(harry);
+ Assert.Equal(4, harry!.Files.Count);
+ Assert.Equal(2, harry!.Extras.Count);
}
[Fact]
@@ -68,13 +69,13 @@ namespace Jellyfin.Naming.Tests.Video
"300.nfo"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -88,13 +89,13 @@ namespace Jellyfin.Naming.Tests.Video
"300 trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -108,13 +109,13 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer.mp4"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -129,13 +130,13 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer2.mp4"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -149,13 +150,13 @@ namespace Jellyfin.Naming.Tests.Video
"Looper.2012.bluray.720p.x264.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -173,13 +174,13 @@ namespace Jellyfin.Naming.Tests.Video
"My video 5.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
}
@@ -193,13 +194,13 @@ namespace Jellyfin.Naming.Tests.Video
@"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = true,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = true,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -214,13 +215,13 @@ namespace Jellyfin.Naming.Tests.Video
@"My movie #2.mp4"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = true,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = true,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -235,13 +236,13 @@ namespace Jellyfin.Naming.Tests.Video
@"No (2012) part1-trailer.mp4"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -256,13 +257,13 @@ namespace Jellyfin.Naming.Tests.Video
@"No (2012)-trailer.mp4"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -278,13 +279,13 @@ namespace Jellyfin.Naming.Tests.Video
@"trailer.mp4"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -300,13 +301,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -319,13 +320,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -338,13 +339,13 @@ namespace Jellyfin.Naming.Tests.Video
@"The Colony.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -358,13 +359,13 @@ namespace Jellyfin.Naming.Tests.Video
@"Four Sisters and a Wedding - B.avi"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -378,13 +379,13 @@ namespace Jellyfin.Naming.Tests.Video
@"Four Rooms - A.mp4"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -398,13 +399,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Server/Despicable Me/movie-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -420,13 +421,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(4, result.Count);
}
@@ -440,13 +441,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Movies/Despicable Me/trailers/trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -457,10 +458,5 @@ namespace Jellyfin.Naming.Tests.Video
var stack = new FileStack();
Assert.False(stack.ContainsFile("XX", true));
}
-
- private VideoListResolver GetResolver()
- {
- return new VideoListResolver(_namingOptions);
- }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index ba5eaf1af..ac5a7a21e 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -9,9 +9,9 @@ namespace Jellyfin.Naming.Tests.Video
{
public class VideoResolverTests
{
- private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
+ private static NamingOptions _namingOptions = new NamingOptions();
- public static IEnumerable<object[]> GetResolveFileTestData()
+ public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
{
yield return new object[]
{
@@ -148,7 +148,7 @@ namespace Jellyfin.Naming.Tests.Video
yield return new object[]
{
new VideoFileInfo(
- path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.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)
@@ -156,30 +156,30 @@ namespace Jellyfin.Naming.Tests.Video
}
[Theory]
- [MemberData(nameof(GetResolveFileTestData))]
+ [MemberData(nameof(ResolveFile_ValidFileNameTestData))]
public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult)
{
- var result = _videoResolver.ResolveFile(expectedResult.Path);
+ var result = VideoResolver.ResolveFile(expectedResult.Path, _namingOptions);
Assert.NotNull(result);
- Assert.Equal(result?.Path, expectedResult.Path);
- Assert.Equal(result?.Container, expectedResult.Container);
- Assert.Equal(result?.Name, expectedResult.Name);
- Assert.Equal(result?.Year, expectedResult.Year);
- Assert.Equal(result?.ExtraType, expectedResult.ExtraType);
- Assert.Equal(result?.Format3D, expectedResult.Format3D);
- Assert.Equal(result?.Is3D, expectedResult.Is3D);
- Assert.Equal(result?.IsStub, expectedResult.IsStub);
- Assert.Equal(result?.StubType, expectedResult.StubType);
- Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory);
- Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
- Assert.Equal(result?.ToString(), expectedResult.ToString());
+ Assert.Equal(result!.Path, expectedResult.Path);
+ Assert.Equal(result.Container, expectedResult.Container);
+ Assert.Equal(result.Name, expectedResult.Name);
+ Assert.Equal(result.Year, expectedResult.Year);
+ Assert.Equal(result.ExtraType, expectedResult.ExtraType);
+ Assert.Equal(result.Format3D, expectedResult.Format3D);
+ Assert.Equal(result.Is3D, expectedResult.Is3D);
+ Assert.Equal(result.IsStub, expectedResult.IsStub);
+ Assert.Equal(result.StubType, expectedResult.StubType);
+ Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
+ Assert.Equal(result.FileNameWithoutExtension.ToString(), expectedResult.FileNameWithoutExtension.ToString());
+ Assert.Equal(result.ToString(), expectedResult.ToString());
}
[Fact]
public void ResolveFile_EmptyPath()
{
- var result = _videoResolver.ResolveFile(string.Empty);
+ var result = VideoResolver.ResolveFile(string.Empty, _namingOptions);
Assert.Null(result);
}
@@ -194,12 +194,16 @@ namespace Jellyfin.Naming.Tests.Video
string.Empty
};
- var results = paths.Select(path => _videoResolver.ResolveDirectory(path)).ToList();
+ var results = paths.Select(path => VideoResolver.ResolveDirectory(path, _namingOptions)).ToList();
Assert.Equal(3, results.Count);
Assert.NotNull(results[0]);
Assert.NotNull(results[1]);
Assert.Null(results[2]);
+ foreach (var result in results)
+ {
+ Assert.Null(result?.Container);
+ }
}
}
}
diff --git a/tests/Jellyfin.Networking.Tests/IPHostTests.cs b/tests/Jellyfin.Networking.Tests/IPHostTests.cs
new file mode 100644
index 000000000..ec3a1300c
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/IPHostTests.cs
@@ -0,0 +1,53 @@
+using FsCheck;
+using FsCheck.Xunit;
+using MediaBrowser.Common.Net;
+using Xunit;
+
+namespace Jellyfin.Networking.Tests
+{
+ public static class IPHostTests
+ {
+ /// <summary>
+ /// Checks IP address formats.
+ /// </summary>
+ /// <param name="address">IP Address.</param>
+ [Theory]
+ [InlineData("127.0.0.1")]
+ [InlineData("127.0.0.1:123")]
+ [InlineData("localhost")]
+ [InlineData("localhost:1345")]
+ [InlineData("www.google.co.uk")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
+ [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(IPHost.TryParse(address, out _));
+
+ [Property]
+ public static Property TryParse_IPv4Address_True(IPv4Address address)
+ => IPHost.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ [Property]
+ public static Property TryParse_IPv6Address_True(IPv6Address address)
+ => IPHost.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ /// <summary>
+ /// All should be invalid address strings.
+ /// </summary>
+ /// <param name="address">Invalid address strings.</param>
+ [Theory]
+ [InlineData("256.128.0.0.0.1")]
+ [InlineData("127.0.0.1#")]
+ [InlineData("localhost!")]
+ [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(IPHost.TryParse(address, out _));
+ }
+}
diff --git a/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs
new file mode 100644
index 000000000..aa2dbc57a
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs
@@ -0,0 +1,49 @@
+using FsCheck;
+using FsCheck.Xunit;
+using MediaBrowser.Common.Net;
+using Xunit;
+
+namespace Jellyfin.Networking.Tests
+{
+ public static class IPNetAddressTests
+ {
+ /// <summary>
+ /// Checks IP address formats.
+ /// </summary>
+ /// <param name="address">IP Address.</param>
+ [Theory]
+ [InlineData("127.0.0.1")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
+ [InlineData("192.168.1.2/255.255.255.0")]
+ [InlineData("192.168.1.2/24")]
+ public static void TryParse_ValidIPStrings_True(string address)
+ => Assert.True(IPNetAddress.TryParse(address, out _));
+
+ [Property]
+ public static Property TryParse_IPv4Address_True(IPv4Address address)
+ => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ [Property]
+ public static Property TryParse_IPv6Address_True(IPv6Address address)
+ => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ /// <summary>
+ /// All should be invalid address strings.
+ /// </summary>
+ /// <param name="address">Invalid address strings.</param>
+ [Theory]
+ [InlineData("256.128.0.0.0.1")]
+ [InlineData("127.0.0.1#")]
+ [InlineData("localhost!")]
+ [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(IPNetAddress.TryParse(address, out _));
+ }
+}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index d77645cd9..97bf673ae 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -8,32 +8,35 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
- <Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
- <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
+ <PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
+
<ItemGroup>
- <ProjectReference Include="..\..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
- <ProjectReference Include="..\..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../../Emby.Server.Implementations/Emby.Server.Implementations.csproj" />
+ <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
+
</Project>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
new file mode 100644
index 000000000..1cad625b7
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
@@ -0,0 +1,63 @@
+using System.Net;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Manager;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Jellyfin.Networking.Tests
+{
+ public class NetworkManagerTests
+ {
+ /// <summary>
+ /// Checks that the given IP address is in the specified network(s).
+ /// </summary>
+ /// <param name="network">Network address(es).</param>
+ /// <param name="value">The IP to check.</param>
+ [Theory]
+ [InlineData("192.168.2.1/24", "192.168.2.123")]
+ [InlineData("192.168.2.1/24, !192.168.2.122/32", "192.168.2.123")]
+ [InlineData("fd23:184f:2029:0::/56", "fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d518/128", "fd23:184f:2029:0:3139:7386:67d7:d517")]
+ public void InNetwork_True_Success(string network, string value)
+ {
+ var ip = IPAddress.Parse(value);
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV6 = true,
+ EnableIPV4 = true,
+ LocalNetworkSubnets = network.Split(',')
+ };
+
+ using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+ Assert.True(networkManager.IsInLocalNetwork(ip));
+ }
+
+ /// <summary>
+ /// Checks that thge given IP address is not in the network provided.
+ /// </summary>
+ /// <param name="network">Network address(es).</param>
+ /// <param name="value">The IP to check.</param>
+ [Theory]
+ [InlineData("192.168.10.0/24", "192.168.11.1")]
+ [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")]
+ [InlineData("192.168.10.0/24", "fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0::/56", "fd24:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d500/120", "fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0::/56", "192.168.10.60")]
+ public void InNetwork_False_Success(string network, string value)
+ {
+ var ip = IPAddress.Parse(value);
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV6 = true,
+ EnableIPV4 = true,
+ LocalNetworkSubnets = network.Split(',')
+ };
+
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+ Assert.False(nm.IsInLocalNetwork(ip));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index b7c1510d2..97c14d463 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -1,47 +1,19 @@
using System;
+using System.Collections.ObjectModel;
using System.Net;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
-using Moq;
using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
using Xunit;
-using System.Collections.ObjectModel;
namespace Jellyfin.Networking.Tests
{
public class NetworkParseTests
{
- /// <summary>
- /// Tries to identify the string and return an object of that class.
- /// </summary>
- /// <param name="addr">String to parse.</param>
- /// <param name="result">IPObject to return.</param>
- /// <returns>True if the value parsed successfully.</returns>
- private static bool TryParse(string addr, out IPObject result)
- {
- if (!string.IsNullOrEmpty(addr))
- {
- // Is it an IP address
- if (IPNetAddress.TryParse(addr, out IPNetAddress nw))
- {
- result = nw;
- return true;
- }
-
- if (IPHost.TryParse(addr, out IPHost h))
- {
- result = h;
- return true;
- }
- }
-
- result = IPNetAddress.None;
- return false;
- }
-
- private static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
+ internal static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
{
var configManager = new Mock<IConfigurationManager>
{
@@ -52,15 +24,22 @@ namespace Jellyfin.Networking.Tests
}
/// <summary>
- /// Checks the ability to ignore interfaces
+ /// Checks the ability to ignore virtual interfaces.
/// </summary>
/// <param name="interfaces">Mock network setup, in the format (IP address, interface index, interface name) | .... </param>
/// <param name="lan">LAN addresses.</param>
/// <param name="value">Bind addresses that are excluded.</param>
[Theory]
+ // All valid
[InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")]
+ // eth16 only
[InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
- [InlineData("192.168.1.208/24,-16,vEthernet1|192.168.1.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
+ // All interfaces excluded. (including loopbacks)
+ [InlineData("192.168.1.208/24,-16,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[127.0.0.1/8,::1/128]")]
+ // vEthernet1 and vEthernet212 should be excluded.
+ [InlineData("192.168.1.200/24,-20,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.200/24", "[200.200.200.200/24,127.0.0.1/8,::1/128]")]
+ // Overlapping interface,
+ [InlineData("192.168.1.110/24,-20,br0|192.168.1.10/24,-16,br0|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.110/24,192.168.1.10/24]")]
public void IgnoreVirtualInterfaces(string interfaces, string lan, string value)
{
var conf = new NetworkConfiguration()
@@ -78,71 +57,6 @@ namespace Jellyfin.Networking.Tests
}
/// <summary>
- /// Check that the value given is in the network provided.
- /// </summary>
- /// <param name="network">Network address.</param>
- /// <param name="value">Value to check.</param>
- [Theory]
- [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")]
- public void IsInNetwork(string network, string value)
- {
- if (network == null)
- {
- throw new ArgumentNullException(nameof(network));
- }
-
- var conf = new NetworkConfiguration()
- {
- EnableIPV6 = true,
- EnableIPV4 = true,
- LocalNetworkSubnets = network.Split(',')
- };
-
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
-
- Assert.False(nm.IsInLocalNetwork(value));
- }
-
- /// <summary>
- /// Checks IP address formats.
- /// </summary>
- /// <param name="address"></param>
- [Theory]
- [InlineData("127.0.0.1")]
- [InlineData("127.0.0.1:123")]
- [InlineData("localhost")]
- [InlineData("localhost:1345")]
- [InlineData("www.google.co.uk")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
- [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
- [InlineData("fe80::7add:12ff:febb:c67b%16")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
- [InlineData("192.168.1.2/255.255.255.0")]
- [InlineData("192.168.1.2/24")]
- public void ValidIPStrings(string address)
- {
- Assert.True(TryParse(address, out _));
- }
-
-
- /// <summary>
- /// All should be invalid address strings.
- /// </summary>
- /// <param name="address">Invalid address strings.</param>
- [Theory]
- [InlineData("256.128.0.0.0.1")]
- [InlineData("127.0.0.1#")]
- [InlineData("localhost!")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
- [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
- public void InvalidAddressString(string address)
- {
- Assert.False(TryParse(address, out _));
- }
-
-
- /// <summary>
/// Test collection parsing.
/// </summary>
/// <param name="settings">Collection to parse.</param>
@@ -152,19 +66,22 @@ namespace Jellyfin.Networking.Tests
/// <param name="result4">Excluded IP4 addresses from the collection.</param>
/// <param name="result5">Network addresses of the collection.</param>
[Theory]
- [InlineData("127.0.0.1#",
+ [InlineData(
+ "127.0.0.1#",
"[]",
"[]",
"[]",
"[]",
"[]")]
- [InlineData("!127.0.0.1",
+ [InlineData(
+ "!127.0.0.1",
"[]",
"[]",
"[127.0.0.1/32]",
"[127.0.0.1/32]",
"[]")]
- [InlineData("",
+ [InlineData(
+ "",
"[]",
"[]",
"[]",
@@ -172,12 +89,13 @@ namespace Jellyfin.Networking.Tests
"[]")]
[InlineData(
"192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, !10.10.10.10",
- "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]",
+ "[192.158.1.2/16,[127.0.0.1/32,::1/128],fd23:184f:2029:0:3139:7386:67d7:d517/128]",
"[192.158.1.2/16,127.0.0.1/32]",
"[10.10.10.10/32]",
"[10.10.10.10/32]",
- "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")]
- [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8",
+ "[192.158.0.0/16,127.0.0.1/32,::1/128,fd23:184f:2029:0:3139:7386:67d7:d517/128]")]
+ [InlineData(
+ "192.158.1.2/255.255.0.0,192.169.1.2/8",
"[192.158.1.2/16,192.169.1.2/8]",
"[192.158.1.2/16,192.169.1.2/8]",
"[]",
@@ -194,34 +112,34 @@ namespace Jellyfin.Networking.Tests
{
EnableIPV6 = true,
EnableIPV4 = true,
- };
+ };
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
// Test included.
- Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false);
+ Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(','), false);
Assert.Equal(nc.AsString(), result1);
// Test excluded.
- nc = nm.CreateIPCollection(settings.Split(","), true);
+ nc = nm.CreateIPCollection(settings.Split(','), true);
Assert.Equal(nc.AsString(), result3);
conf.EnableIPV6 = false;
nm.UpdateSettings(conf);
-
+
// Test IP4 included.
- nc = nm.CreateIPCollection(settings.Split(","), false);
+ nc = nm.CreateIPCollection(settings.Split(','), false);
Assert.Equal(nc.AsString(), result2);
// Test IP4 excluded.
- nc = nm.CreateIPCollection(settings.Split(","), true);
+ nc = nm.CreateIPCollection(settings.Split(','), true);
Assert.Equal(nc.AsString(), result4);
conf.EnableIPV6 = true;
nm.UpdateSettings(conf);
// Test network addresses of collection.
- nc = nm.CreateIPCollection(settings.Split(","), false);
+ nc = nm.CreateIPCollection(settings.Split(','), false);
nc = nc.AsNetworks();
Assert.Equal(nc.AsString(), result5);
}
@@ -252,7 +170,6 @@ namespace Jellyfin.Networking.Tests
throw new ArgumentNullException(nameof(result));
}
-
var conf = new NetworkConfiguration()
{
EnableIPV6 = true,
@@ -261,10 +178,10 @@ namespace Jellyfin.Networking.Tests
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
- Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false);
- Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false);
+ Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(','), false);
+ Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(','), false);
- Assert.Equal(nc1.Union(nc2).AsString(), result);
+ Assert.Equal(nc1.ThatAreContainedInNetworks(nc2).AsString(), result);
}
[Theory]
@@ -333,8 +250,8 @@ namespace Jellyfin.Networking.Tests
public void TestSubnetContains(string network, string ip)
{
- Assert.True(TryParse(network, out IPObject? networkObj));
- Assert.True(TryParse(ip, out IPObject? ipObj));
+ Assert.True(IPNetAddress.TryParse(network, out var networkObj));
+ Assert.True(IPNetAddress.TryParse(ip, out var ipObj));
Assert.True(networkObj.Contains(ipObj));
}
@@ -371,14 +288,13 @@ namespace Jellyfin.Networking.Tests
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
// Test included, IP6.
- Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(","));
- Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(","));
- Collection<IPObject> ncResult = ncSource.Union(ncDest);
- Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(","));
+ Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(','));
+ Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(','));
+ Collection<IPObject> ncResult = ncSource.ThatAreContainedInNetworks(ncDest);
+ Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(','));
Assert.True(ncResult.Compare(resultCollection));
}
-
[Theory]
[InlineData("10.1.1.1/32", "10.1.1.1")]
[InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")]
@@ -408,6 +324,9 @@ namespace Jellyfin.Networking.Tests
[InlineData("jellyfin.org", "eth16", false, "eth16")]
// User on external network, no binding - so result is the 1st external.
[InlineData("jellyfin.org", "", false, "eth11")]
+ // Dns failure - should skip the test.
+ // https://en.wikipedia.org/wiki/.test
+ [InlineData("invalid.domain.test", "", false, "eth11")]
// User assumed to be internal, no binding - so result is the 1st internal.
[InlineData("", "", false, "eth16")]
public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result)
@@ -440,10 +359,13 @@ namespace Jellyfin.Networking.Tests
_ = nm.TryParseInterface(result, out Collection<IPObject>? resultObj);
- if (resultObj != null)
+ // Check to see if dns resolution is working. If not, skip test.
+ _ = IPHost.TryParse(source, out var host);
+
+ if (resultObj != null && host?.HasAddress == true)
{
result = ((IPNetAddress)resultObj[0]).ToString(true);
- var intf = nm.GetBindInterface(source, out int? _);
+ var intf = nm.GetBindInterface(source, out _);
Assert.Equal(intf, result);
}
@@ -455,7 +377,7 @@ namespace Jellyfin.Networking.Tests
// On my system eth16 is internal, eth11 external (Windows defines the indexes).
//
// This test is to replicate how subnet bound ServerPublisherUri work throughout the system.
-
+
// User on internal network, we're bound internal and external - so result is internal override.
[InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
@@ -468,7 +390,7 @@ namespace Jellyfin.Networking.Tests
// User on internal network, no binding specified - so result is the 1st internal.
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
- // User on external network, internal binding only - so asumption is a proxy forward, return external override.
+ // User on external network, internal binding only - so assumption is a proxy forward, return external override.
[InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
// User on external network, no binding - so result is the 1st external which is overriden.
@@ -479,7 +401,6 @@ namespace Jellyfin.Networking.Tests
// User is internal, no binding - so result is the 1st internal, which is then overridden.
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
-
public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
{
if (lan == null)
@@ -515,5 +436,45 @@ namespace Jellyfin.Networking.Tests
Assert.Equal(intf, result);
}
+
+ [Theory]
+ [InlineData("185.10.10.10,200.200.200.200", "79.2.3.4", true)]
+ [InlineData("185.10.10.10", "185.10.10.10", false)]
+ [InlineData("", "100.100.100.100", false)]
+
+ public void HasRemoteAccess_GivenWhitelist_AllowsOnlyIpsInWhitelist(string addresses, string remoteIp, bool denied)
+ {
+ // 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.
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV4 = true,
+ RemoteIPFilter = addresses.Split(','),
+ IsRemoteIPFilterBlacklist = false
+ };
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+ Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied);
+ }
+
+ [Theory]
+ [InlineData("185.10.10.10", "79.2.3.4", false)]
+ [InlineData("185.10.10.10", "185.10.10.10", true)]
+ [InlineData("", "100.100.100.100", false)]
+ public void HasRemoteAccess_GivenBlacklist_BlacklistTheIps(string addresses, string remoteIp, bool denied)
+ {
+ // 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.
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV4 = true,
+ RemoteIPFilter = addresses.Split(','),
+ IsRemoteIPFilterBlacklist = true
+ };
+
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+ Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied);
+ }
}
}
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
new file mode 100644
index 000000000..14bd53db5
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -0,0 +1,37 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ <PackageReference Include="coverlet.collector" Version="3.0.3">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <!-- Code Analyzers -->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
new file mode 100644
index 000000000..b160e676e
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
@@ -0,0 +1,96 @@
+#pragma warning disable CA1002 // Do not expose generic lists
+
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Providers.MediaInfo;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo
+{
+ public class SubtitleResolverTests
+ {
+ public static IEnumerable<object[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
+ {
+ var index = 0;
+ yield return new object[]
+ {
+ new List<MediaStream>(),
+ "/video/My.Video.mkv",
+ index,
+ new[]
+ {
+ "/video/My.Video.mp3",
+ "/video/My.Video.png",
+ "/video/My.Video.srt",
+ "/video/My.Video.txt",
+ "/video/My.Video.vtt",
+ "/video/My.Video.ass",
+ "/video/My.Video.sub",
+ "/video/My.Video.ssa",
+ "/video/My.Video.smi",
+ "/video/My.Video.sami",
+ "/video/My.Video.en.srt",
+ "/video/My.Video.default.en.srt",
+ "/video/My.Video.default.forced.en.srt",
+ "/video/My.Video.en.default.forced.srt",
+ "/video/My.Video.With.Additional.Garbage.en.srt",
+ "/video/My.Video With Additional Garbage.srt"
+ },
+ new[]
+ {
+ CreateMediaStream("/video/My.Video.srt", "srt", null, index++),
+ CreateMediaStream("/video/My.Video.vtt", "vtt", null, index++),
+ CreateMediaStream("/video/My.Video.ass", "ass", null, index++),
+ CreateMediaStream("/video/My.Video.sub", "sub", null, index++),
+ CreateMediaStream("/video/My.Video.ssa", "ssa", null, index++),
+ CreateMediaStream("/video/My.Video.smi", "smi", null, index++),
+ CreateMediaStream("/video/My.Video.sami", "sami", null, index++),
+ CreateMediaStream("/video/My.Video.en.srt", "srt", "en", index++),
+ CreateMediaStream("/video/My.Video.default.en.srt", "srt", "en", index++, isDefault: true),
+ CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true),
+ CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true),
+ CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index),
+ }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData))]
+ public void AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles(List<MediaStream> streams, string videoPath, int startIndex, string[] files, MediaStream[] expectedResult)
+ {
+ new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
+
+ Assert.Equal(expectedResult.Length, streams.Count);
+ for (var i = 0; i < expectedResult.Length; i++)
+ {
+ var expected = expectedResult[i];
+ var actual = streams[i];
+
+ Assert.Equal(expected.Index, actual.Index);
+ Assert.Equal(expected.Type, actual.Type);
+ Assert.Equal(expected.IsExternal, actual.IsExternal);
+ Assert.Equal(expected.Path, actual.Path);
+ Assert.Equal(expected.IsDefault, actual.IsDefault);
+ Assert.Equal(expected.IsForced, actual.IsForced);
+ Assert.Equal(expected.Language, actual.Language);
+ }
+ }
+
+ private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false)
+ {
+ return new ()
+ {
+ Index = index,
+ Codec = codec,
+ Type = MediaStreamType.Subtitle,
+ IsExternal = true,
+ Path = path,
+ IsDefault = isDefault,
+ IsForced = isForced,
+ Language = language
+ };
+ }
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs
new file mode 100644
index 000000000..f6a7c676f
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs
@@ -0,0 +1,27 @@
+using MediaBrowser.Providers.Plugins.Tmdb;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.Tmdb
+{
+ public static class TmdbUtilsTests
+ {
+ [Theory]
+ [InlineData("de", "de")]
+ [InlineData("En", "En")]
+ [InlineData("de-de", "de-DE")]
+ [InlineData("en-US", "en-US")]
+ [InlineData("de-CH", "de")]
+ public static void NormalizeLanguage_Valid_Success(string input, string expected)
+ {
+ Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input));
+ }
+
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData("", "")]
+ public static void NormalizeLanguage_Invalid_Equal(string? input, string? expected)
+ {
+ Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input!));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
new file mode 100644
index 000000000..f312933fb
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections.Generic;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.Data;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Data
+{
+ public class SqliteItemRepositoryTests
+ {
+ public const string VirtualMetaDataPath = "%MetadataPath%";
+ public const string MetaDataPath = "/meta/data/path";
+
+ private readonly IFixture _fixture;
+ private readonly SqliteItemRepository _sqliteItemRepository;
+
+ public SqliteItemRepositoryTests()
+ {
+ var appHost = new Mock<IServerApplicationHost>();
+ appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>()))
+ .Returns((string x) => x.Replace(VirtualMetaDataPath, MetaDataPath, StringComparison.Ordinal));
+ appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
+ .Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal));
+
+ _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ _fixture.Inject(appHost);
+ _sqliteItemRepository = _fixture.Create<SqliteItemRepository>();
+ }
+
+ public static IEnumerable<object[]> ItemImageInfoFromValueString_Valid_TestData()
+ {
+ yield return new object[]
+ {
+ "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
+ new ItemImageInfo
+ {
+ Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
+ Type = ImageType.Primary,
+ DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
+ Width = 1920,
+ Height = 1080,
+ BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
+ }
+ };
+
+ yield return new object[]
+ {
+ "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*0*0",
+ new ItemImageInfo
+ {
+ Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
+ Type = ImageType.Primary,
+ }
+ };
+
+ yield return new object[]
+ {
+ "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary",
+ new ItemImageInfo
+ {
+ Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
+ Type = ImageType.Primary,
+ }
+ };
+
+ yield return new object[]
+ {
+ "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*600",
+ new ItemImageInfo
+ {
+ Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
+ Type = ImageType.Primary,
+ }
+ };
+
+ yield return new object[]
+ {
+ "%MetadataPath%/library/68/68578562b96c80a7ebd530848801f645/poster.jpg*637264380567586027*Primary*600*336",
+ new ItemImageInfo
+ {
+ Path = "/meta/data/path/library/68/68578562b96c80a7ebd530848801f645/poster.jpg",
+ Type = ImageType.Primary,
+ DateModified = new DateTime(637264380567586027, DateTimeKind.Utc),
+ Width = 600,
+ Height = 336
+ }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(ItemImageInfoFromValueString_Valid_TestData))]
+ public void ItemImageInfoFromValueString_Valid_Success(string value, ItemImageInfo expected)
+ {
+ var result = _sqliteItemRepository.ItemImageInfoFromValueString(value);
+ Assert.Equal(expected.Path, result.Path);
+ Assert.Equal(expected.Type, result.Type);
+ Assert.Equal(expected.DateModified, result.DateModified);
+ Assert.Equal(expected.Width, result.Width);
+ Assert.Equal(expected.Height, result.Height);
+ Assert.Equal(expected.BlurHash, result.BlurHash);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("*")]
+ [InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")]
+ public void ItemImageInfoFromValueString_Invalid_Null(string value)
+ {
+ Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));
+ }
+
+ public static IEnumerable<object[]> DeserializeImages_Valid_TestData()
+ {
+ yield return new object[]
+ {
+ "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
+ new ItemImageInfo[]
+ {
+ new ItemImageInfo()
+ {
+ Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
+ Type = ImageType.Primary,
+ DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
+ Width = 1920,
+ Height = 1080,
+ BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
+ }
+ }
+ };
+
+ yield return new object[]
+ {
+ "%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg*637261226720645297*Primary*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png*637261226720805297*Logo*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg*637261226721285297*Thumb*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg*637261226721685297*Backdrop*0*0",
+ new ItemImageInfo[]
+ {
+ new ItemImageInfo()
+ {
+ Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg",
+ Type = ImageType.Primary,
+ DateModified = new DateTime(637261226720645297, DateTimeKind.Utc),
+ },
+ new ItemImageInfo()
+ {
+ Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png",
+ Type = ImageType.Logo,
+ DateModified = new DateTime(637261226720805297, DateTimeKind.Utc),
+ },
+ new ItemImageInfo()
+ {
+ Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg",
+ Type = ImageType.Thumb,
+ DateModified = new DateTime(637261226721285297, DateTimeKind.Utc),
+ },
+ new ItemImageInfo()
+ {
+ Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg",
+ Type = ImageType.Backdrop,
+ DateModified = new DateTime(637261226721685297, DateTimeKind.Utc),
+ }
+ }
+ };
+ }
+
+ public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData()
+ {
+ yield return new object[]
+ {
+ string.Empty,
+ Array.Empty<ItemImageInfo>()
+ };
+
+ yield return new object[]
+ {
+ "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss",
+ new ItemImageInfo[]
+ {
+ new ()
+ {
+ Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
+ Type = ImageType.Primary,
+ DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
+ Width = 1920,
+ Height = 1080,
+ BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
+ }
+ }
+ };
+
+ yield return new object[]
+ {
+ "|",
+ Array.Empty<ItemImageInfo>()
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(DeserializeImages_Valid_TestData))]
+ public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected)
+ {
+ var result = _sqliteItemRepository.DeserializeImages(value);
+ Assert.Equal(expected.Length, result.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i].Path, result[i].Path);
+ Assert.Equal(expected[i].Type, result[i].Type);
+ Assert.Equal(expected[i].DateModified, result[i].DateModified);
+ Assert.Equal(expected[i].Width, result[i].Width);
+ Assert.Equal(expected[i].Height, result[i].Height);
+ Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))]
+ public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected)
+ {
+ var result = _sqliteItemRepository.DeserializeImages(value);
+ Assert.Equal(expected.Length, result.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i].Path, result[i].Path);
+ Assert.Equal(expected[i].Type, result[i].Type);
+ Assert.Equal(expected[i].DateModified, result[i].DateModified);
+ Assert.Equal(expected[i].Width, result[i].Width);
+ Assert.Equal(expected[i].Height, result[i].Height);
+ Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(DeserializeImages_Valid_TestData))]
+ public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value)
+ {
+ Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value));
+ }
+
+ public static IEnumerable<object[]> DeserializeProviderIds_Valid_TestData()
+ {
+ yield return new object[]
+ {
+ "Imdb=tt0119567",
+ new Dictionary<string, string>()
+ {
+ { "Imdb", "tt0119567" },
+ }
+ };
+
+ yield return new object[]
+ {
+ "Imdb=tt0119567|Tmdb=330|TmdbCollection=328",
+ new Dictionary<string, string>()
+ {
+ { "Imdb", "tt0119567" },
+ { "Tmdb", "330" },
+ { "TmdbCollection", "328" },
+ }
+ };
+
+ yield return new object[]
+ {
+ "MusicBrainzAlbum=9d363e43-f24f-4b39-bc5a-7ef305c677c7|MusicBrainzReleaseGroup=63eba062-847c-3b73-8b0f-6baf27bba6fa|AudioDbArtist=111352|AudioDbAlbum=2116560|MusicBrainzAlbumArtist=20244d07-534f-4eff-b4d4-930878889970",
+ new Dictionary<string, string>()
+ {
+ { "MusicBrainzAlbum", "9d363e43-f24f-4b39-bc5a-7ef305c677c7" },
+ { "MusicBrainzReleaseGroup", "63eba062-847c-3b73-8b0f-6baf27bba6fa" },
+ { "AudioDbArtist", "111352" },
+ { "AudioDbAlbum", "2116560" },
+ { "MusicBrainzAlbumArtist", "20244d07-534f-4eff-b4d4-930878889970" },
+ }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(DeserializeProviderIds_Valid_TestData))]
+ public void DeserializeProviderIds_Valid_Success(string value, Dictionary<string, string> expected)
+ {
+ var result = new ProviderIdsExtensionsTestsObject();
+ SqliteItemRepository.DeserializeProviderIds(value, result);
+ Assert.Equal(expected, result.ProviderIds);
+ }
+
+ [Theory]
+ [MemberData(nameof(DeserializeProviderIds_Valid_TestData))]
+ public void SerializeProviderIds_Valid_Success(string expected, Dictionary<string, string> values)
+ {
+ Assert.Equal(expected, SqliteItemRepository.SerializeProviderIds(values));
+ }
+
+ private class ProviderIdsExtensionsTestsObject : IHasProviderIds
+ {
+ public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
index 671c59b2e..30e6542f9 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
@@ -1,3 +1,6 @@
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.InteropServices;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.IO;
@@ -38,5 +41,36 @@ namespace Jellyfin.Server.Implementations.Tests.IO
Assert.Equal(expectedAbsolutePath, generatedPath);
}
}
+
+ [Theory]
+ [InlineData("ValidFileName", "ValidFileName")]
+ [InlineData("AC/DC", "AC DC")]
+ [InlineData("Invalid\0", "Invalid ")]
+ [InlineData("AC/DC\0KD/A", "AC DC KD A")]
+ public void GetValidFilename_ReturnsValidFilename(string filename, string expectedFileName)
+ {
+ Assert.Equal(expectedFileName, _sut.GetValidFilename(filename));
+ }
+
+ [SkippableFact]
+ public void GetFileInfo_DanglingSymlink_ExistsFalse()
+ {
+ Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
+
+ string testFileDir = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
+ string testFileName = Path.Combine(testFileDir, Path.GetRandomFileName() + "-danglingsym.link");
+
+ Directory.CreateDirectory(testFileDir);
+ Assert.Equal(0, symlink("thispathdoesntexist", testFileName));
+ Assert.True(File.Exists(testFileName));
+
+ var metadata = _sut.GetFileInfo(testFileName);
+ Assert.False(metadata.Exists);
+ }
+
+ [SuppressMessage("Naming Rules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Have to")]
+ [DllImport("libc", SetLastError = true, CharSet = CharSet.Ansi)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.UserDirectories)]
+ private static extern int symlink(string target, string linkpath);
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index 174f29b09..adbca8344 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -10,6 +10,8 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
<RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
</PropertyGroup>
@@ -20,18 +22,18 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="AutoFixture" Version="4.15.0" />
- <PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
- <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="AutoFixture" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
+ <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -39,10 +41,8 @@
<ItemGroup>
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
+ <ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
+ <ProjectReference Include="..\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
new file mode 100644
index 000000000..c393742eb
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -0,0 +1,72 @@
+using System;
+using Emby.Server.Implementations.Library.Resolvers.TV;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library
+{
+ public class EpisodeResolverTest
+ {
+ [Fact]
+ public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode()
+ {
+ var season = new Season { Name = "Season 1" };
+ var parent = new Folder { Name = "extras" };
+ var libraryManagerMock = new Mock<ILibraryManager>();
+ libraryManagerMock.Setup(x => x.GetItemById(It.IsAny<Guid>())).Returns(season);
+
+ var episodeResolver = new EpisodeResolver(libraryManagerMock.Object);
+ var itemResolveArgs = new ItemResolveArgs(
+ Mock.Of<IServerApplicationPaths>(),
+ Mock.Of<IDirectoryService>())
+ {
+ Parent = parent,
+ CollectionType = CollectionType.TvShows,
+ FileInfo = new FileSystemMetadata()
+ {
+ FullName = "All My Children/Season 01/Extras/All My Children S01E01 - Behind The Scenes.mkv"
+ }
+ };
+
+ Assert.Null(episodeResolver.Resolve(itemResolveArgs));
+ }
+
+ [Fact]
+ public void Resolve_GivenVideoInExtrasSeriesFolder_ResolvesToEpisode()
+ {
+ var series = new Series { Name = "Extras" };
+
+ // Have to create a mock because of moq proxies not being castable to a concrete implementation
+ // https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48
+ var episodeResolver = new EpisodeResolverMock(Mock.Of<ILibraryManager>());
+ var itemResolveArgs = new ItemResolveArgs(
+ Mock.Of<IServerApplicationPaths>(),
+ Mock.Of<IDirectoryService>())
+ {
+ Parent = series,
+ CollectionType = CollectionType.TvShows,
+ FileInfo = new FileSystemMetadata()
+ {
+ FullName = "Extras/Extras S01E01.mkv"
+ }
+ };
+ Assert.NotNull(episodeResolver.Resolve(itemResolveArgs));
+ }
+
+ private class EpisodeResolverMock : EpisodeResolver
+ {
+ public EpisodeResolverMock(ILibraryManager libraryManager) : base(libraryManager)
+ {
+ }
+
+ protected override TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) => new ();
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index 6d768af89..c5cc056f5 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -24,5 +24,36 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
Assert.Throws<ArgumentException>(() => PathExtensions.GetAttributeValue(input, attribute));
}
+
+ [Theory]
+ [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("/o", "/o", "/s", "/s")] // regression test for #5977
+ public void TryReplaceSubPath_ValidArgs_Correct(string path, string subPath, string newSubPath, string? expectedResult)
+ {
+ Assert.True(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData(null, null, null)]
+ [InlineData(null, "/my/path", "/another/path")]
+ [InlineData("/my/path", null, "/another/path")]
+ [InlineData("/my/path", "/another/path", null)]
+ [InlineData("", "", "")]
+ [InlineData("/my/path", "", "")]
+ [InlineData("", "/another/path", "")]
+ [InlineData("", "", "/new/subpath")]
+ [InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/not jeff's band", "/home/not jeff")]
+ public void TryReplaceSubPath_InvalidInput_ReturnsFalseAndNull(string? path, string? subPath, string? newSubPath)
+ {
+ Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
+ Assert.Null(result);
+ }
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
index 8847239d9..c859d11c6 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
@@ -15,8 +16,6 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
public class HdHomerunHostTests
{
- private const string TestIp = "http://192.168.1.182";
-
private readonly Fixture _fixture;
private readonly HdHomerunHost _hdHomerunHost;
@@ -30,7 +29,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
return Task.FromResult(new HttpResponseMessage()
{
- Content = new StreamContent(File.OpenRead("Test Data/LiveTv/" + m.RequestUri?.Segments[^1]))
+ Content = new StreamContent(File.OpenRead(Path.Combine("Test Data/LiveTv", m.RequestUri!.Host, m.RequestUri.Segments[^1])))
});
});
@@ -50,7 +49,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
var host = new TunerHostInfo()
{
- Url = TestIp
+ Url = "192.168.1.182"
};
var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false);
@@ -66,6 +65,26 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
}
[Fact]
+ public async Task GetModelInfo_Legacy_Success()
+ {
+ var host = new TunerHostInfo()
+ {
+ Url = "10.10.10.100"
+ };
+
+ var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal("HDHomeRun DUAL", modelInfo.FriendlyName);
+ Assert.Equal("HDHR3-US", modelInfo.ModelNumber);
+ Assert.Equal("hdhomerun3_atsc", modelInfo.FirmwareName);
+ Assert.Equal("20200225", modelInfo.FirmwareVersion);
+ Assert.Equal("10xxxxx5", modelInfo.DeviceID);
+ Assert.Null(modelInfo.DeviceAuth);
+ Assert.Equal(2, modelInfo.TunerCount);
+ Assert.Equal("http://10.10.10.100:80", modelInfo.BaseURL);
+ Assert.Null(modelInfo.LineupURL);
+ }
+
+ [Fact]
public async Task GetModelInfo_EmptyUrl_ArgumentException()
{
var host = new TunerHostInfo()
@@ -81,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
var host = new TunerHostInfo()
{
- Url = TestIp
+ Url = "192.168.1.182"
};
var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false);
@@ -94,11 +113,23 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
}
[Fact]
+ public async Task GetLineup_Legacy_Success()
+ {
+ var host = new TunerHostInfo()
+ {
+ Url = "10.10.10.100"
+ };
+
+ // Placeholder json is invalid, just need to make sure we can reach it
+ await Assert.ThrowsAsync<JsonException>(() => _hdHomerunHost.GetLineup(host, CancellationToken.None));
+ }
+
+ [Fact]
public async Task GetLineup_ImportFavoritesOnly_Success()
{
var host = new TunerHostInfo()
{
- Url = TestIp,
+ Url = "192.168.1.182",
ImportFavoritesOnly = true
};
@@ -114,9 +145,9 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
[Fact]
public async Task TryGetTunerHostInfo_Valid_Success()
{
- var host = await _hdHomerunHost.TryGetTunerHostInfo(TestIp, CancellationToken.None).ConfigureAwait(false);
+ var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None).ConfigureAwait(false);
Assert.Equal(_hdHomerunHost.Type, host.Type);
- Assert.Equal(TestIp, host.Url);
+ Assert.Equal("192.168.1.182", host.Url);
Assert.Equal("HDHomeRun PRIME", host.FriendlyName);
Assert.Equal("FFFFFFFF", host.DeviceId);
Assert.Equal(3, host.TunerCount);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs
new file mode 100644
index 000000000..fd499d9cf
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs
@@ -0,0 +1,326 @@
+using System;
+using System.Text;
+using Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.LiveTv
+{
+ public class HdHomerunManagerTests
+ {
+ [Fact]
+ public void WriteNullTerminatedString_Empty_Success()
+ {
+ ReadOnlySpan<byte> expected = stackalloc byte[]
+ {
+ 1, 0
+ };
+
+ Span<byte> buffer = stackalloc byte[128];
+ int len = HdHomerunManager.WriteNullTerminatedString(buffer, string.Empty);
+
+ Assert.Equal(
+ Convert.ToHexString(expected),
+ Convert.ToHexString(buffer.Slice(0, len)));
+ }
+
+ [Fact]
+ public void WriteNullTerminatedString_Valid_Success()
+ {
+ ReadOnlySpan<byte> expected = stackalloc byte[]
+ {
+ 10, (byte)'T', (byte)'h', (byte)'e', (byte)' ', (byte)'q', (byte)'u', (byte)'i', (byte)'c', (byte)'k', 0
+ };
+
+ Span<byte> buffer = stackalloc byte[128];
+ int len = HdHomerunManager.WriteNullTerminatedString(buffer, "The quick");
+
+ Assert.Equal(
+ Convert.ToHexString(expected),
+ Convert.ToHexString(buffer.Slice(0, len)));
+ }
+
+ [Fact]
+ public void WriteGetMessage_Valid_Success()
+ {
+ ReadOnlySpan<byte> expected = stackalloc byte[]
+ {
+ 0, 4,
+ 0, 12,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 0xc0, 0xc9, 0x87, 0x33
+ };
+
+ Span<byte> buffer = stackalloc byte[128];
+ int len = HdHomerunManager.WriteGetMessage(buffer, 0, "N");
+
+ Assert.Equal(
+ Convert.ToHexString(expected),
+ Convert.ToHexString(buffer.Slice(0, len)));
+ }
+
+ [Fact]
+ public void WriteSetMessage_NoLockKey_Success()
+ {
+ ReadOnlySpan<byte> expected = stackalloc byte[]
+ {
+ 0, 4,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0xa9, 0x49, 0xd0, 0x68
+ };
+
+ Span<byte> buffer = stackalloc byte[128];
+ int len = HdHomerunManager.WriteSetMessage(buffer, 0, "N", "value", null);
+
+ Assert.Equal(
+ Convert.ToHexString(expected),
+ Convert.ToHexString(buffer.Slice(0, len)));
+ }
+
+ [Fact]
+ public void WriteSetMessage_LockKey_Success()
+ {
+ ReadOnlySpan<byte> expected = stackalloc byte[]
+ {
+ 0, 4,
+ 0, 26,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 21,
+ 4, 0x00, 0x01, 0x38, 0xd5,
+ 0x8e, 0xb6, 0x06, 0x82
+ };
+
+ Span<byte> buffer = stackalloc byte[128];
+ int len = HdHomerunManager.WriteSetMessage(buffer, 0, "N", "value", 80085);
+
+ Assert.Equal(
+ Convert.ToHexString(expected),
+ Convert.ToHexString(buffer.Slice(0, len)));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_Valid_Success()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0x7d, 0xa3, 0xa3, 0xf3
+ };
+
+ Assert.True(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out var value));
+ Assert.Equal("value", Encoding.UTF8.GetString(value));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_InvalidCrc_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0x7d, 0xa3, 0xa3, 0xf4
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_InvalidPacketType_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 4,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0xa9, 0x49, 0xd0, 0x68
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_InvalidPacket_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 0x7d, 0xa3, 0xa3
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_TooSmallMessageLength_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 19,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0x25, 0x25, 0x44, 0x9a
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_TooLargeMessageLength_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 21,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0xe3, 0x20, 0x79, 0x6c
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_TooLargeNameLength_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 3,
+ 20, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0xe1, 0x8e, 0x9c, 0x74
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_InvalidGetSetNameTag_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 4,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0xee, 0x05, 0xe7, 0x12
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_InvalidGetSetValueTag_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 3,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0x64, 0xaa, 0x66, 0xf9
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void TryGetReturnValueOfGetSet_TooLargeValueLength_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 7, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0xc9, 0xa8, 0xd4, 0x55
+ };
+
+ Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
+ }
+
+ [Fact]
+ public void VerifyReturnValueOfGetSet_Valid_True()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0x7d, 0xa3, 0xa3, 0xf3
+ };
+
+ Assert.True(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "value"));
+ }
+
+ [Fact]
+ public void VerifyReturnValueOfGetSet_WrongValue_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 5,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0x7d, 0xa3, 0xa3, 0xf3
+ };
+
+ Assert.False(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "none"));
+ }
+
+ [Fact]
+ public void VerifyReturnValueOfGetSet_InvalidPacket_False()
+ {
+ ReadOnlySpan<byte> packet = new byte[]
+ {
+ 0, 4,
+ 0, 20,
+ 3,
+ 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
+ 4,
+ 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
+ 0x7d, 0xa3, 0xa3, 0xf3
+ };
+
+ Assert.False(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "value"));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
new file mode 100644
index 000000000..e8b93b437
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using Emby.Server.Implementations.LiveTv.EmbyTV;
+using MediaBrowser.Controller.LiveTv;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.LiveTv
+{
+ public static class RecordingHelperTests
+ {
+ public static IEnumerable<object[]> GetRecordingName_Success_TestData()
+ {
+ yield return new object[]
+ {
+ "The Incredibles 2020_04_20_21_06_00",
+ new TimerInfo
+ {
+ Name = "The Incredibles",
+ StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
+ IsMovie = true
+ }
+ };
+
+ yield return new object[]
+ {
+ "The Incredibles (2004)",
+ new TimerInfo
+ {
+ Name = "The Incredibles",
+ IsMovie = true,
+ ProductionYear = 2004
+ }
+ };
+
+ yield return new object[]
+ {
+ "The Big Bang Theory 2020_04_20_21_06_00",
+ new TimerInfo
+ {
+ Name = "The Big Bang Theory",
+ StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
+ IsProgramSeries = true,
+ }
+ };
+
+ yield return new object[]
+ {
+ "The Big Bang Theory S12E10",
+ new TimerInfo
+ {
+ Name = "The Big Bang Theory",
+ IsProgramSeries = true,
+ SeasonNumber = 12,
+ EpisodeNumber = 10
+ }
+ };
+
+ yield return new object[]
+ {
+ "The Big Bang Theory S12E10 The VCR Illumination",
+ new TimerInfo
+ {
+ Name = "The Big Bang Theory",
+ IsProgramSeries = true,
+ SeasonNumber = 12,
+ EpisodeNumber = 10,
+ EpisodeTitle = "The VCR Illumination"
+ }
+ };
+
+ yield return new object[]
+ {
+ "The Big Bang Theory 2018-12-06",
+ new TimerInfo
+ {
+ Name = "The Big Bang Theory",
+ IsProgramSeries = true,
+ OriginalAirDate = new DateTime(2018, 12, 6)
+ }
+ };
+
+ yield return new object[]
+ {
+ "The Big Bang Theory 2018-12-06 - The VCR Illumination",
+ new TimerInfo
+ {
+ Name = "The Big Bang Theory",
+ IsProgramSeries = true,
+ OriginalAirDate = new DateTime(2018, 12, 6),
+ EpisodeTitle = "The VCR Illumination"
+ }
+ };
+
+ yield return new object[]
+ {
+ "The Big Bang Theory 2018_12_06_21_06_00 - The VCR Illumination",
+ new TimerInfo
+ {
+ Name = "The Big Bang Theory",
+ StartDate = new DateTime(2018, 12, 6, 21, 6, 0, DateTimeKind.Local),
+ IsProgramSeries = true,
+ OriginalAirDate = new DateTime(2018, 12, 6),
+ EpisodeTitle = "The VCR Illumination"
+ }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(GetRecordingName_Success_TestData))]
+ public static void GetRecordingName_Success(string expected, TimerInfo timerInfo)
+ {
+ Assert.Equal(expected, RecordingHelper.GetRecordingName(timerInfo));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json
new file mode 100644
index 000000000..a4ad4ed44
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json
@@ -0,0 +1 @@
+{"FriendlyName":"HDHomeRun DUAL","ModelNumber":"HDHR3-US","Legacy":1,"FirmwareName":"hdhomerun3_atsc","FirmwareVersion":"20200225","DeviceID":"10xxxxx5","TunerCount":2,"BaseURL":"http://10.10.10.100:80"}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/discover.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json
index 851f17bb2..851f17bb2 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/discover.json
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/lineup.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json
index 4cb5ebc8e..4cb5ebc8e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/lineup.json
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json
new file mode 100644
index 000000000..b766e668e
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json
@@ -0,0 +1,684 @@
+[
+ {
+ "guid": "a4df60c5-6ab4-412a-8f79-2cab93fb2bc5",
+ "name": "Anime",
+ "description": "Manage your anime in Jellyfin. This plugin supports several different metadata providers and options for organizing your collection.\n",
+ "overview": "Manage your anime from Jellyfin",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "10.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_10.0.0.0.zip",
+ "checksum": "93e969adeba1050423fc8817ed3c36f8",
+ "timestamp": "2020-08-17T01:41:13Z"
+ },
+ {
+ "version": "9.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_9.0.0.0.zip",
+ "checksum": "9b1cebff835813e15f414f44b40c41c8",
+ "timestamp": "2020-07-20T01:30:16Z"
+ }
+ ]
+ },
+ {
+ "guid": "70b7b43b-471b-4159-b4be-56750c795499",
+ "name": "Auto Organize",
+ "description": "Automatically organize your media",
+ "overview": "Automatically organize your media",
+ "owner": "jellyfin",
+ "category": "General",
+ "versions": [
+ {
+ "version": "9.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/auto-organize/auto-organize_9.0.0.0.zip",
+ "checksum": "ff29ac3cbe05d208b6af94cd6d9dea39",
+ "timestamp": "2020-12-05T22:31:12Z"
+ },
+ {
+ "version": "8.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/auto-organize/auto-organize_8.0.0.0.zip",
+ "checksum": "460bbb45e556464a8476b18e41c097f5",
+ "timestamp": "2020-07-20T01:30:25Z"
+ }
+ ]
+ },
+ {
+ "guid": "9c4e63f1-031b-4f25-988b-4f7d78a8b53e",
+ "name": "Bookshelf",
+ "description": "Supports several different metadata providers and options for organizing your collection.\n",
+ "overview": "Manage your books",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "5.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/bookshelf/bookshelf_5.0.0.0.zip",
+ "checksum": "2063fb8ab317b8d77b200fde41eb5e1e",
+ "timestamp": "2020-12-05T22:03:13Z"
+ },
+ {
+ "version": "4.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/bookshelf/bookshelf_4.0.0.0.zip",
+ "checksum": "fc9f76c0815d766491e5b0f30ede55ed",
+ "timestamp": "2020-07-20T01:30:33Z"
+ }
+ ]
+ },
+ {
+ "guid": "cfa0f7f4-4155-4d71-849b-d6598dc4c5bb",
+ "name": "Email",
+ "description": "Send SMTP email notifications",
+ "overview": "Send SMTP email notifications",
+ "owner": "jellyfin",
+ "category": "Notifications",
+ "versions": [
+ {
+ "version": "9.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/email/email_9.0.0.0.zip",
+ "checksum": "cfe7afc00f3fbd6d6ab8244d7ff968ce",
+ "timestamp": "2020-12-05T22:20:32Z"
+ },
+ {
+ "version": "7.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/email/email_7.0.0.0.zip",
+ "checksum": "680ca511d8ad84923cb04f024fd8eb19",
+ "timestamp": "2020-07-20T01:30:40Z"
+ }
+ ]
+ },
+ {
+ "guid": "170a157f-ac6c-437a-abdd-ca9c25cebd39",
+ "name": "Fanart",
+ "description": "Scrape poster images for movies, shows, and artists in your library.",
+ "overview": "Scrape poster images from Fanart",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "6.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/fanart/fanart_6.0.0.0.zip",
+ "checksum": "ee4360bfcc8722d5a3a54cfe7eef640f",
+ "timestamp": "2020-12-05T22:25:43Z"
+ },
+ {
+ "version": "5.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/fanart/fanart_5.0.0.0.zip",
+ "checksum": "f842f7d65d23f377761c907d40b89647",
+ "timestamp": "2020-07-20T01:30:48Z"
+ }
+ ]
+ },
+ {
+ "guid": "e29621a5-fa9e-4330-982e-ef6e54c0cad2",
+ "name": "Gotify Notification",
+ "description": "You must have a Gotify server to use this plugin!\n",
+ "overview": "Sends notifications to your Gotify server",
+ "owner": "crobibero",
+ "category": "Notifications",
+ "versions": [
+ {
+ "version": "7.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/gotify-notification/gotify-notification_7.0.0.0.zip",
+ "checksum": "7c5ff9e8792c8cdee7e8a2aaeb6cc093",
+ "timestamp": "2020-07-20T01:30:56Z"
+ }
+ ]
+ },
+ {
+ "guid": "a59b5c4b-05a8-488f-bfa8-7a63fffc7639",
+ "name": "IPTV",
+ "description": "Enable IPTV support in Jellyfin",
+ "overview": "Enable IPTV support in Jellyfin",
+ "owner": "jellyfin",
+ "category": "Channel",
+ "versions": [
+ {
+ "version": "6.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/iptv/iptv_6.0.0.0.zip",
+ "checksum": "9cf103bf67a4eda7c3a42d9b235f6447",
+ "timestamp": "2020-07-20T01:31:05Z"
+ }
+ ]
+ },
+ {
+ "guid": "4682DD4C-A675-4F1B-8E7C-79ADF137A8F8",
+ "name": "ISO Mounter",
+ "description": "Mount your ISO files for Jellyfin.\n",
+ "overview": "Mount your ISO files for Jellyfin",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "1.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/iso-mounter/iso-mounter_1.0.0.0.zip",
+ "checksum": "847e5bc7ac34c1bf4dc5b28173170fae",
+ "timestamp": "2020-07-20T01:31:13Z"
+ }
+ ]
+ },
+ {
+ "guid": "771e19d6-5385-4caf-b35c-28a0e865cf63",
+ "name": "Kodi Sync Queue",
+ "description": "This plugin will track all media changes while Kodi clients are offline to decrease sync times.",
+ "overview": "Sync all media changes with Kodi clients",
+ "owner": "jellyfin",
+ "category": "General",
+ "versions": [
+ {
+ "version": "6.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_6.0.0.0.zip",
+ "checksum": "787c856c0d2ad2224cdd8b3094cf0329",
+ "timestamp": "2020-12-05T22:10:37Z"
+ },
+ {
+ "version": "5.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_5.0.0.0.zip",
+ "checksum": "08285397aecd93ea64a4f15d38b1bd7b",
+ "timestamp": "2020-07-20T01:31:22Z"
+ }
+ ]
+ },
+ {
+ "guid": "958aad66-3784-4d2a-b89a-a7b6fab6e25c",
+ "name": "LDAP Authentication",
+ "description": "Authenticate your Jellyfin users against an LDAP database, and optionally create users who do not yet exist automatically.\nAllows the administrator to customize most aspects of the LDAP authentication process, including customizable search attributes, username attribute, and a search filter for administrative users (set on user creation). The user, via the \"Manual Login\" process, can enter any valid attribute value, which will be mapped back to the specified username attribute automatically as well.\n",
+ "overview": "Authenticate users against an LDAP database",
+ "owner": "jellyfin",
+ "category": "Authentication",
+ "versions": [
+ {
+ "version": "10.0.0.0",
+ "changelog": "Update for 10.7 support\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_10.0.0.0.zip",
+ "checksum": "62e7e1cd3ffae0944c14750a3c90df4f",
+ "timestamp": "2020-12-05T19:48:10Z"
+ },
+ {
+ "version": "9.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_9.0.0.0.zip",
+ "checksum": "7f2f83587a65a43ebf168e4058421463",
+ "timestamp": "2020-07-22T15:42:57Z"
+ },
+ {
+ "version": "8.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_8.0.0.0.zip",
+ "checksum": "8af8cee62717d63577f8b1e710839415",
+ "timestamp": "2020-07-20T01:31:30Z"
+ }
+ ]
+ },
+ {
+ "guid": "9574ac10-bf23-49bc-949f-924f23cfa48f",
+ "name": "NextPVR",
+ "description": "Provides access to live TV, program guide, and recordings from NextPVR.\n",
+ "overview": "Live TV plugin for NextPVR",
+ "owner": "jellyfin",
+ "category": "LiveTV",
+ "versions": [
+ {
+ "version": "5.0.0.0",
+ "changelog": "Updated to use NextPVR API v5, no longer compatable with API v4.\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/nextpvr/nextpvr_5.0.0.0.zip",
+ "checksum": "d70f694d14bf9462ba2b2ebe110068d3",
+ "timestamp": "2020-12-05T22:24:03Z"
+ },
+ {
+ "version": "4.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/nextpvr/nextpvr_4.0.0.0.zip",
+ "checksum": "b15949d895ac5a8c89496581db350478",
+ "timestamp": "2020-07-20T01:31:38Z"
+ }
+ ]
+ },
+ {
+ "guid": "4b9ed42f-5185-48b5-9803-6ff2989014c4",
+ "name": "Open Subtitles",
+ "description": "Download subtitles from the internet to use with your media files.",
+ "overview": "Download subtitles for your media",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "10.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/open-subtitles/open-subtitles_10.0.0.0.zip",
+ "checksum": "ed99d03ec463bf15fca1256a113f57b4",
+ "timestamp": "2020-12-05T21:56:19Z"
+ },
+ {
+ "version": "9.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/open-subtitles/open-subtitles_9.0.0.0.zip",
+ "checksum": "16789b26497cea0509daf6b18c579340",
+ "timestamp": "2020-07-20T01:32:00Z"
+ }
+ ]
+ },
+ {
+ "guid": "5c534381-91a3-43cb-907a-35aa02eb9d2c",
+ "name": "Playback Reporting",
+ "description": "Collect and show user play statistics",
+ "overview": "Collect and show user play statistics",
+ "owner": "jellyfin",
+ "category": "General",
+ "versions": [
+ {
+ "version": "9.0.0.0",
+ "changelog": "Add authentication to plugin endpoints\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_9.0.0.0.zip",
+ "checksum": "ca323b3dcb2cb86cc2e72a7a0f1eee22",
+ "timestamp": "2020-12-05T22:15:48Z"
+ },
+ {
+ "version": "8.0.0.0",
+ "changelog": "Add authentication to plugin endpoints\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_8.0.0.0.zip",
+ "checksum": "58644c505586542ef0b8b65e2f704bd1",
+ "timestamp": "2020-11-18T03:01:51Z"
+ },
+ {
+ "version": "7.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_7.0.0.0.zip",
+ "checksum": "6a361ef33bca97f9155856d02ff47380",
+ "timestamp": "2020-07-20T01:32:09Z"
+ }
+ ]
+ },
+ {
+ "guid": "de228f12-e43e-4bd9-9fc0-2830819c3b92",
+ "name": "Pushbullet",
+ "description": "Get notifications via Pushbullet.\n",
+ "overview": "Pushbullet notification plugin",
+ "owner": "jellyfin",
+ "category": "Notifications",
+ "versions": [
+ {
+ "version": "6.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushbullet/pushbullet_6.0.0.0.zip",
+ "checksum": "248cf3d56644f1d909e75aaddbdfb3a6",
+ "timestamp": "2020-12-06T02:47:53Z"
+ },
+ {
+ "version": "5.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushbullet/pushbullet_5.0.0.0.zip",
+ "checksum": "dabbdd86328b2922a69dfa0c9e1c8343",
+ "timestamp": "2020-07-20T01:32:17Z"
+ }
+ ]
+ },
+ {
+ "guid": "F240D6BE-5743-441B-87F1-A70ECAC42642",
+ "name": "Pushover",
+ "description": "Send messages to a wide range of devices through Pushover.",
+ "overview": "Send notifications via Pushover",
+ "owner": "crobibero",
+ "category": "Notifications",
+ "versions": [
+ {
+ "version": "4.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushover/pushover_4.0.0.0.zip",
+ "checksum": "56a0da16c7e48cc184987737b7e155dd",
+ "timestamp": "2020-07-20T01:32:25Z"
+ }
+ ]
+ },
+ {
+ "guid": "d4312cd9-5c90-4f38-82e8-51da566790e8",
+ "name": "Reports",
+ "description": "Generate reports of your media library",
+ "overview": "Generate reports of your media library",
+ "owner": "jellyfin",
+ "category": "General",
+ "versions": [
+ {
+ "version": "11.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_11.0.0.0.zip",
+ "checksum": "d71bc6a4c008e58ee70ad44c83bfd310",
+ "timestamp": "2020-12-05T22:00:46Z"
+ },
+ {
+ "version": "10.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_10.0.0.0.zip",
+ "checksum": "3917e75839337475b42daf2ba0b5bd7b",
+ "timestamp": "2020-10-19T19:30:41Z"
+ },
+ {
+ "version": "9.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_9.0.0.0.zip",
+ "checksum": "5b5ad8d885616a21e8d1e8eecf5ea979",
+ "timestamp": "2020-10-16T23:52:37Z"
+ }
+ ]
+ },
+ {
+ "guid": "1fc322a1-af2e-49a5-b2eb-a89b4240f700",
+ "name": "ServerWMC",
+ "description": "Provides access to Live TV, Program Guide and Recordings from your Windows MediaCenter Server running ServerWMC. Requires ServerWMC to be installed and running on your Windows MediaCenter machine.\n",
+ "overview": "Jellyfin Live TV plugin for Windows MediaCenter with ServerWMC",
+ "owner": "jellyfin",
+ "category": "LiveTV",
+ "versions": [
+ {
+ "version": "6.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/serverwmc/serverwmc_6.0.0.0.zip",
+ "checksum": "3120af0cea2c1cb8b7cf578d9b4b862c",
+ "timestamp": "2020-12-05T22:28:15Z"
+ },
+ {
+ "version": "5.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/serverwmc/serverwmc_5.0.0.0.zip",
+ "checksum": "dc44b039aa1b66eaf40a44fbf02d37e2",
+ "timestamp": "2020-07-20T01:32:42Z"
+ }
+ ]
+ },
+ {
+ "guid": "94fb77c3-55ad-4c50-bf4e-4e5497467b79",
+ "name": "Slack Notifications",
+ "description": "Get notifications via Slack.\n",
+ "overview": "Get notifications via Slack",
+ "owner": "jellyfin",
+ "category": "Notifications",
+ "versions": [
+ {
+ "version": "7.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/slack-notifications/slack-notifications_7.0.0.0.zip",
+ "checksum": "1d5330a77ce7b2a9ac8e5d58088a012c",
+ "timestamp": "2020-12-05T22:40:02Z"
+ },
+ {
+ "version": "6.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/slack-notifications/slack-notifications_6.0.0.0.zip",
+ "checksum": "ede4cbe064542d1ecccc5823921bee4b",
+ "timestamp": "2020-07-20T01:32:50Z"
+ }
+ ]
+ },
+ {
+ "guid": "bc4aad2e-d3d0-4725-a5e2-fd07949e5b42",
+ "name": "TMDb Box Sets",
+ "description": "Automatically create movie box sets based on TMDb collections",
+ "overview": "Automatically create movie box sets based on TMDb collections",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "7.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tmdb-box-sets/tmdb-box-sets_7.0.0.0.zip",
+ "checksum": "1551792e6af4d36f2cead01153c73cf0",
+ "timestamp": "2020-12-05T22:07:21Z"
+ },
+ {
+ "version": "6.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tmdb-box-sets/tmdb-box-sets_6.0.0.0.zip",
+ "checksum": "b92b68a922c5fcbb8f4d47b8601b01b6",
+ "timestamp": "2020-07-20T01:32:58Z"
+ }
+ ]
+ },
+ {
+ "guid": "4fe3201e-d6ae-4f2e-8917-e12bda571281",
+ "name": "Trakt",
+ "description": "Record your watched media with Trakt.\n",
+ "overview": "Record your watched media with Trakt",
+ "owner": "jellyfin",
+ "category": "General",
+ "versions": [
+ {
+ "version": "11.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/trakt/trakt_11.0.0.0.zip",
+ "checksum": "2257ccde1e39114644a27e0966a0bf2d",
+ "timestamp": "2020-12-05T19:56:12Z"
+ },
+ {
+ "version": "10.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/trakt/trakt_10.0.0.0.zip",
+ "checksum": "ab67e6b59ea2e7860a6a3ff7b8452759",
+ "timestamp": "2020-07-20T01:33:06Z"
+ }
+ ]
+ },
+ {
+ "guid": "3fd018e5-5e78-4e58-b280-a0c068febee0",
+ "name": "TVHeadend",
+ "description": "Manage TVHeadend from Jellyfin",
+ "overview": "Manage TVHeadend from Jellyfin",
+ "owner": "jellyfin",
+ "category": "LiveTV",
+ "versions": [
+ {
+ "version": "7.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tvheadend/tvheadend_7.0.0.0.zip",
+ "checksum": "1abbfce737b6962f4b1b2255dc63e932",
+ "timestamp": "2021-01-05T16:20:33Z"
+ },
+ {
+ "version": "6.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tvheadend/tvheadend_6.0.0.0.zip",
+ "checksum": "143c34fd70d7173b8912cc03ce4b517d",
+ "timestamp": "2020-07-20T01:33:15Z"
+ }
+ ]
+ },
+ {
+ "guid": "022a3003-993f-45f1-8565-87d12af2e12a",
+ "name": "InfuseSync",
+ "description": "This plugin will track all media changes while any Infuse clients are offline to decrease sync times when logging back in to your server.",
+ "overview": "Blazing fast indexing for Infuse",
+ "owner": "Firecore LLC",
+ "category": "General",
+ "versions": [
+ {
+ "version": "1.2.4.0",
+ "changelog": "New Playlist support.\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.4/InfuseSync-jellyfin-1.2.4.zip",
+ "checksum": "7adde11b8c8404fd2923f59d98fb1a30",
+ "timestamp": "2020-10-12T08:00:00Z"
+ },
+ {
+ "version": "1.2.1.3",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.3/InfuseSync-jellyfin-1.2.3.zip",
+ "checksum": "d8e2c5fe736a302097bb3bac3d04b1c4",
+ "timestamp": "2020-09-18T12:19:00Z"
+ },
+ {
+ "version": "1.2.1.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.1/InfuseSync-jellyfin-1.2.1.zip",
+ "checksum": "1a853e926cc422f5d79d398d9ae18ee8",
+ "timestamp": "2020-08-21T10:48:00Z"
+ },
+ {
+ "version": "1.2.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.0/InfuseSync-jellyfin-1.2.0.zip",
+ "checksum": "2d3c7859852695a7f05adc6d3fcbc783",
+ "timestamp": "2020-07-20T11:51:00Z"
+ }
+ ]
+ },
+ {
+ "guid": "8119f3c6-cfc2-4d9c-a0ba-028f1d93e526",
+ "name": "Cover Art Archive",
+ "description": "This plugin provides images from the Cover Art Archive https://musicbrainz.org/doc/Cover_Art_Archive and depends on the MusicBrainz metadata provider to know what images belong where\n",
+ "overview": "MusicBrainz Cover Art Archive",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "2.0.0.0",
+ "changelog": "changelog\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/cover-art-archive/cover-art-archive_2.0.0.0.zip",
+ "checksum": "bea8fa4a37b3e7ed74e22266e7597a68",
+ "timestamp": "2020-12-06T02:51:03Z"
+ },
+ {
+ "version": "1.0.0.3",
+ "changelog": "changelog\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/cover-art-archive/cover-art-archive_1.0.0.3.zip",
+ "checksum": "c502a5c54b168810614c1c40709b9598",
+ "timestamp": "2020-08-06T21:21:22Z"
+ }
+ ]
+ },
+ {
+ "guid": "A4A488D0-17A3-4919-8D82-7F3DE4F6B209",
+ "name": "TV Maze",
+ "description": "Get TV metadata from TV Maze\n",
+ "overview": "Get TV metadata from TV Maze",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "5.0.0.0",
+ "changelog": "Get additional image types\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_5.0.0.0.zip",
+ "checksum": "509a85e40b1d1ac36eef45673deaf606",
+ "timestamp": "2020-12-06T02:51:56Z"
+ },
+ {
+ "version": "4.0.0.0",
+ "changelog": "Get additional image types\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_4.0.0.0.zip",
+ "checksum": "58ee9ab3f129151bdfff033ad889ad87",
+ "timestamp": "2020-11-24T14:44:37Z"
+ },
+ {
+ "version": "3.0.0.0",
+ "changelog": "Remove unused dependencies \n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_3.0.0.0.zip",
+ "checksum": "f3b2c70b3e136fb15c917e4420f4fdec",
+ "timestamp": "2020-11-09T14:32:56Z"
+ },
+ {
+ "version": "2.0.0.0",
+ "changelog": "Remove unused dependencies \n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_2.0.0.0.zip",
+ "checksum": "c7662ae8ae52ce8a4e8d685d55f36e80",
+ "timestamp": "2020-11-09T02:33:11Z"
+ },
+ {
+ "version": "1.0.0.0",
+ "changelog": "Initial release.\n",
+ "targetAbi": "10.6.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_1.0.0.0.zip",
+ "checksum": "c90eee48c12f2c07880b4b28e507fd14",
+ "timestamp": "2020-11-08T19:05:32Z"
+ }
+ ]
+ },
+ {
+ "guid": "a677c0da-fac5-4cde-941a-7134223f14c8",
+ "name": "TheTVDB",
+ "description": "Get TV metadata from TheTvdb\n",
+ "overview": "Get TV metadata from TheTvdb",
+ "owner": "jellyfin",
+ "category": "Metadata",
+ "versions": [
+ {
+ "version": "2.0.0.0",
+ "changelog": "Remove from Jellyfin core.\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/thetvdb/thetvdb_2.0.0.0.zip",
+ "checksum": "e46cee334476a1b475e5c553171c4cb6",
+ "timestamp": "2020-12-16T20:03:28Z"
+ },
+ {
+ "version": "1.0.0.0",
+ "changelog": "Remove from Jellyfin core.\n",
+ "targetAbi": "10.7.0.0",
+ "sourceUrl": "https://repo.jellyfin.org/releases/plugin/thetvdb/thetvdb_1.0.0.0.zip",
+ "checksum": "5a3dca5c0db4824d83bfd4e7e2b7bf11",
+ "timestamp": "2020-12-06T02:56:40Z"
+ }
+ ]
+ }
+] \ No newline at end of file
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
new file mode 100644
index 000000000..70acbfc40
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.Updates;
+using MediaBrowser.Model.Updates;
+using Moq;
+using Moq.Protected;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Updates
+{
+ public class InstallationManagerTests
+ {
+ private readonly Fixture _fixture;
+ private readonly InstallationManager _installationManager;
+
+ public InstallationManagerTests()
+ {
+ var messageHandler = new Mock<HttpMessageHandler>();
+ messageHandler.Protected()
+ .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
+ .Returns<HttpRequestMessage, CancellationToken>(
+ (m, _) =>
+ {
+ return Task.FromResult(new HttpResponseMessage()
+ {
+ Content = new StreamContent(File.OpenRead("Test Data/Updates/" + m.RequestUri?.Segments[^1]))
+ });
+ });
+
+ var http = new Mock<IHttpClientFactory>();
+ http.Setup(x => x.CreateClient(It.IsAny<string>()))
+ .Returns(new HttpClient(messageHandler.Object));
+ _fixture = new Fixture();
+ _fixture.Customize(new AutoMoqCustomization
+ {
+ ConfigureMembers = true
+ }).Inject(http);
+ _installationManager = _fixture.Create<InstallationManager>();
+ }
+
+ [Fact]
+ public async Task GetPackages_Valid_Success()
+ {
+ PackageInfo[] packages = await _installationManager.GetPackages(
+ "Jellyfin Stable",
+ "https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
+ false);
+
+ Assert.Equal(25, packages.Length);
+ }
+
+ [Fact]
+ public async Task FilterPackages_NameOnly_Success()
+ {
+ PackageInfo[] packages = await _installationManager.GetPackages(
+ "Jellyfin Stable",
+ "https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
+ false);
+
+ packages = _installationManager.FilterPackages(packages, "Anime").ToArray();
+ Assert.Single(packages);
+ }
+
+ [Fact]
+ public async Task FilterPackages_GuidOnly_Success()
+ {
+ PackageInfo[] packages = await _installationManager.GetPackages(
+ "Jellyfin Stable",
+ "https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
+ false);
+
+ packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
+ Assert.Single(packages);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs
new file mode 100644
index 000000000..867dda29d
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs
@@ -0,0 +1,28 @@
+using System;
+using Jellyfin.Server.Implementations.Users;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Users
+{
+ public class UserManagerTests
+ {
+ [Theory]
+ [InlineData("this_is_valid")]
+ [InlineData("this is also valid")]
+ [InlineData("0@_-' .")]
+ public void ThrowIfInvalidUsername_WhenValidUsername_DoesNotThrowArgumentException(string username)
+ {
+ var ex = Record.Exception(() => UserManager.ThrowIfInvalidUsername(username));
+ Assert.Null(ex);
+ }
+
+ [Theory]
+ [InlineData(" ")]
+ [InlineData("")]
+ [InlineData("special characters like & $ ? are not allowed")]
+ public void ThrowIfInvalidUsername_WhenInvalidUsername_ThrowsArgumentException(string username)
+ {
+ Assert.Throws<ArgumentException>(() => UserManager.ThrowIfInvalidUsername(username));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
new file mode 100644
index 000000000..ea6838682
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StartupDtos;
+using Jellyfin.Api.Models.UserDtos;
+using MediaBrowser.Common.Json;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests
+{
+ public static class AuthHelper
+ {
+ public const string AuthHeaderName = "X-Emby-Authorization";
+ public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\"";
+
+ public static async Task<string> CompleteStartupAsync(HttpClient client)
+ {
+ var jsonOptions = JsonDefaults.Options;
+ var userResponse = await client.GetByteArrayAsync("/Startup/User").ConfigureAwait(false);
+ var user = JsonSerializer.Deserialize<StartupUserDto>(userResponse, jsonOptions);
+
+ using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
+
+ using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(
+ new AuthenticateUserByName()
+ {
+ Username = user!.Name,
+ Pw = user.Password,
+ },
+ jsonOptions));
+ content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
+
+ using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content).ConfigureAwait(false);
+ var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
+ await authResponse.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ jsonOptions).ConfigureAwait(false);
+
+ return auth!.AccessToken;
+ }
+
+ public static void AddAuthHeader(this HttpHeaders headers, string accessToken)
+ {
+ headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}");
+ }
+
+ private class AuthenticationResultDto
+ {
+ public string AccessToken { get; set; } = string.Empty;
+
+ public string ServerId { get; set; } = string.Empty;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs
new file mode 100644
index 000000000..be89fbc9a
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs
@@ -0,0 +1,30 @@
+using System.Net;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ public sealed class ActivityLogControllerTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public ActivityLogControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task ActivityLog_GetEntries_Ok()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync("System/ActivityLog/Entries").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs
new file mode 100644
index 000000000..9db8689a7
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs
@@ -0,0 +1,14 @@
+using Jellyfin.Api;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ /// <summary>
+ /// Base controller for testing infrastructure.
+ /// Automatically ignored in generated openapi spec.
+ /// </summary>
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public class BaseJellyfinTestController : BaseJellyfinApiController
+ {
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs
index 1cbe94c5b..87136dfc8 100644
--- a/tests/Jellyfin.Api.Tests/BrandingServiceTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs
@@ -1,15 +1,18 @@
+using System.Net;
+using System.Net.Mime;
+using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using MediaBrowser.Model.Branding;
using Xunit;
-namespace Jellyfin.Api.Tests
+namespace Jellyfin.Server.Integration.Tests
{
- public sealed class BrandingServiceTests : IClassFixture<JellyfinApplicationFactory>
+ public sealed class BrandingControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
- public BrandingServiceTests(JellyfinApplicationFactory factory)
+ public BrandingControllerTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
@@ -24,8 +27,9 @@ namespace Jellyfin.Api.Tests
var response = await client.GetAsync("/Branding/Configuration");
// Assert
- response.EnsureSuccessStatusCode();
- Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
+ 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 responseBody = await response.Content.ReadAsStreamAsync();
_ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody);
}
@@ -42,8 +46,9 @@ namespace Jellyfin.Api.Tests
var response = await client.GetAsync(url);
// Assert
- response.EnsureSuccessStatusCode();
- Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType?.ToString());
+ Assert.True(response.IsSuccessStatusCode);
+ Assert.Equal("text/css", response.Content.Headers.ContentType?.MediaType);
+ Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
}
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
new file mode 100644
index 000000000..f5411dcb8
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
@@ -0,0 +1,86 @@
+using System.IO;
+using System.Net;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models;
+using MediaBrowser.Common.Json;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ public sealed class DashboardControllerTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options;
+
+ public DashboardControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetDashboardConfigurationPage_NonExistingPage_NotFound()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetDashboardConfigurationPage_ExistingPage_CorrectPage()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType);
+ StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!);
+ Assert.Equal(await response.Content.ReadAsStringAsync(), reader.ReadToEnd());
+ }
+
+ [Fact]
+ public async Task GetDashboardConfigurationPage_BrokenPage_NotFound()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetConfigurationPages_NoParams_AllConfigurationPages()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var res = await response.Content.ReadAsStreamAsync();
+ _ = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions);
+ // TODO: check content
+ }
+
+ [Fact]
+ public async Task GetConfigurationPages_True_MainMenuConfigurationPages()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false);
+
+ 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 res = await response.Content.ReadAsStreamAsync();
+ var data = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions);
+ Assert.Empty(data);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs
new file mode 100644
index 000000000..1a720c2f6
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ /// <summary>
+ /// Controller for testing the encoded url.
+ /// </summary>
+ public class EncoderController : BaseJellyfinTestController
+ {
+ /// <summary>
+ /// Tests the url decoding.
+ /// </summary>
+ /// <param name="params">Parameters to echo back in the response.</param>
+ /// <returns>An <see cref="OkResult"/>.</returns>
+ /// <response code="200">Information retrieved.</response>
+ [HttpGet("UrlDecode")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ContentResult TestUrlDecoding([FromQuery] Dictionary<string, string>? @params = null)
+ {
+ return new ContentResult()
+ {
+ Content = (@params != null && @params.Count > 0)
+ ? string.Join("&", @params.Select(x => x.Key + "=" + x.Value))
+ : string.Empty,
+ ContentType = "text/plain; charset=utf-8",
+ StatusCode = 200
+ };
+ }
+
+ /// <summary>
+ /// Tests the url decoding.
+ /// </summary>
+ /// <param name="params">Parameters to echo back in the response.</param>
+ /// <returns>An <see cref="OkResult"/>.</returns>
+ /// <response code="200">Information retrieved.</response>
+ [HttpGet("UrlArrayDecode")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ContentResult TestUrlArrayDecoding([FromQuery] Dictionary<string, string[]>? @params = null)
+ {
+ return new ContentResult()
+ {
+ Content = (@params != null && @params.Count > 0)
+ ? string.Join("&", @params.Select(x => x.Key + "=" + string.Join(',', x.Value)))
+ : string.Empty,
+ ContentType = "text/plain; charset=utf-8",
+ StatusCode = 200
+ };
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
new file mode 100644
index 000000000..169a5a6c5
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.StartupDtos;
+using MediaBrowser.Common.Json;
+using Xunit;
+using Xunit.Priority;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
+ public sealed class StartupControllerTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+
+ public StartupControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ [Priority(-2)]
+ public async Task Configuration_EditConfig_Success()
+ {
+ var client = _factory.CreateClient();
+
+ var config = new StartupConfigurationDto()
+ {
+ UICulture = "NewCulture",
+ MetadataCountryCode = "be",
+ PreferredMetadataLanguage = "nl"
+ };
+
+ using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(config, _jsonOptions));
+ postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ using var postResponse = await client.PostAsync("/Startup/Configuration", postContent).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
+
+ using var getResponse = await client.GetAsync("/Startup/Configuration").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType);
+
+ using var responseStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions).ConfigureAwait(false);
+ Assert.Equal(config.UICulture, newConfig!.UICulture);
+ Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode);
+ Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage);
+ }
+
+ [Fact]
+ [Priority(-2)]
+ public async Task User_DefaultUser_NameWithoutPassword()
+ {
+ var client = _factory.CreateClient();
+
+ using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+
+ using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false);
+ Assert.NotEmpty(user!.Name);
+ Assert.Null(user.Password);
+ }
+
+ [Fact]
+ [Priority(-1)]
+ public async Task User_EditUser_Success()
+ {
+ var client = _factory.CreateClient();
+
+ var user = new StartupUserDto()
+ {
+ Name = "NewName",
+ Password = "NewPassword"
+ };
+
+ using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions));
+ postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ var postResponse = await client.PostAsync("/Startup/User", postContent).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
+
+ var getResponse = await client.GetAsync("/Startup/User").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType);
+
+ var contentStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false);
+ Assert.Equal(user.Name, newUser!.Name);
+ Assert.NotEmpty(newUser.Password);
+ Assert.NotEqual(user.Password, newUser.Password);
+ }
+
+ [Fact]
+ [Priority(0)]
+ public async Task CompleteWizard_Success()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ }
+
+ [Fact]
+ [Priority(1)]
+ public async Task GetFirstUser_CompleteWizard_Unauthorized()
+ {
+ var client = _factory.CreateClient();
+
+ using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
new file mode 100644
index 000000000..6584490de
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.UserDtos;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Model.Dto;
+using Xunit;
+using Xunit.Priority;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
+ public sealed class UserControllerTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private const string TestUsername = "testUser01";
+
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options;
+ private static string? _accessToken;
+ private static Guid _testUserId = Guid.Empty;
+
+ public UserControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ private Task<HttpResponseMessage> CreateUserByName(HttpClient httpClient, CreateUserByName request)
+ {
+ using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions));
+ postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ return httpClient.PostAsync("Users/New", postContent);
+ }
+
+ private Task<HttpResponseMessage> UpdateUserPassword(HttpClient httpClient, Guid userId, UpdateUserPassword request)
+ {
+ using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions));
+ postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ return httpClient.PostAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", postContent);
+ }
+
+ [Fact]
+ [Priority(-1)]
+ public async Task GetPublicUsers_Valid_Success()
+ {
+ var client = _factory.CreateClient();
+
+ using var response = await client.GetAsync("Users/Public").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ // User are hidden by default
+ Assert.Empty(users);
+ }
+
+ [Fact]
+ [Priority(-1)]
+ public async Task GetUsers_Valid_Success()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var response = await client.GetAsync("Users").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ Assert.Single(users);
+ Assert.False(users![0].HasConfiguredPassword);
+ }
+
+ [Fact]
+ [Priority(0)]
+ public async Task New_Valid_Success()
+ {
+ var client = _factory.CreateClient();
+
+ // access token can't be null here as the previous test populated it
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
+
+ var createRequest = new CreateUserByName()
+ {
+ Name = TestUsername
+ };
+
+ using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var user = await JsonSerializer.DeserializeAsync<UserDto>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ Assert.Equal(TestUsername, user!.Name);
+ Assert.False(user.HasPassword);
+ Assert.False(user.HasConfiguredPassword);
+
+ _testUserId = user.Id;
+
+ Console.WriteLine(user.Id.ToString("N", CultureInfo.InvariantCulture));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("‼️")]
+ [Priority(0)]
+ public async Task New_Invalid_Fail(string? username)
+ {
+ var client = _factory.CreateClient();
+
+ // access token can't be null here as the previous test populated it
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
+
+ var createRequest = new CreateUserByName()
+ {
+ Name = username
+ };
+
+ using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ [Priority(1)]
+ public async Task UpdateUserPassword_Valid_Success()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
+
+ var createRequest = new UpdateUserPassword()
+ {
+ NewPw = "4randomPa$$word"
+ };
+
+ using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+
+ var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
+ await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ var user = users!.First(x => x.Id == _testUserId);
+ Assert.True(user.HasPassword);
+ Assert.True(user.HasConfiguredPassword);
+ }
+
+ [Fact]
+ [Priority(2)]
+ public async Task UpdateUserPassword_Empty_RemoveSetPassword()
+ {
+ var client = _factory.CreateClient();
+
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
+
+ var createRequest = new UpdateUserPassword()
+ {
+ CurrentPw = "4randomPa$$word",
+ };
+
+ using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+
+ var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
+ await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ var user = users!.First(x => x.Id == _testUserId);
+ Assert.False(user.HasPassword);
+ Assert.False(user.HasConfiguredPassword);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
new file mode 100644
index 000000000..732b4f050
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
@@ -0,0 +1,48 @@
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests
+{
+ /// <summary>
+ /// Defines the test for encoded querystrings in the url.
+ /// </summary>
+ public class EncodedQueryStringTest : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+
+ public EncodedQueryStringTest(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Theory]
+ [InlineData("a=1&b=2&c=3", "a=1&b=2&c=3")] // won't be processed as there is more than 1.
+ [InlineData("a=1", "a=1")] // won't be processed as it has a value
+ [InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed.
+ [InlineData("a=b&a=c", "a=b")]
+ [InlineData("a%3Db%26a%3Dc", "a=b")]
+ public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ Assert.Equal(unencodedUrl, reply);
+ }
+
+ [Theory]
+ [InlineData("a=b&a=c", "a=b,c")]
+ [InlineData("a%3Db%26a%3Dc", "a=b,c")]
+ public async Task Ensure_Array_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ Assert.Equal(unencodedUrl, reply);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
new file mode 100644
index 000000000..59f125cd0
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -0,0 +1,47 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
+ <PackageReference Include="Xunit.Priority" Version="1.1.6" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="Moq" Version="4.16.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <!-- Don't run tests in parallel -->
+ <None Update="xunit.runner.json">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <!-- Code Analyzers -->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Include="TestPage.html" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index 54f8eb225..d9ec81a27 100644
--- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -1,19 +1,20 @@
using System;
using System.Collections.Concurrent;
using System.IO;
+using System.Threading;
using Emby.Server.Implementations;
using Emby.Server.Implementations.IO;
-using Jellyfin.Server;
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.Logging;
using Serilog;
using Serilog.Extensions.Logging;
-namespace Jellyfin.Api.Tests
+namespace Jellyfin.Server.Integration.Tests
{
/// <summary>
/// Factory for bootstrapping the Jellyfin application in memory for functional end to end tests.
@@ -21,12 +22,12 @@ namespace Jellyfin.Api.Tests
public class JellyfinApplicationFactory : WebApplicationFactory<Startup>
{
private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
- private static readonly ConcurrentBag<IDisposable> _disposableComponents = new ConcurrentBag<IDisposable>();
+ private readonly ConcurrentBag<IDisposable> _disposableComponents = new ConcurrentBag<IDisposable>();
/// <summary>
- /// Initializes a new instance of the <see cref="JellyfinApplicationFactory"/> class.
+ /// Initializes static members of the <see cref="JellyfinApplicationFactory"/> class.
/// </summary>
- public JellyfinApplicationFactory()
+ static JellyfinApplicationFactory()
{
// Perform static initialization that only needs to happen once per test-run
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
@@ -73,10 +74,11 @@ namespace Jellyfin.Api.Tests
_disposableComponents.Add(loggerFactory);
// Create the app host and initialize it
- var appHost = new CoreAppHost(
+ var appHost = new TestAppHost(
appPaths,
loggerFactory,
commandLineOpts,
+ new ConfigurationBuilder().Build(),
new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
_disposableComponents.Add(appHost);
@@ -93,10 +95,10 @@ namespace Jellyfin.Api.Tests
var testServer = base.CreateServer(builder);
// Finish initializing the app host
- var appHost = (CoreAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
+ var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
appHost.ServiceProvider = testServer.Services;
appHost.InitializeServices().GetAwaiter().GetResult();
- appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
+ appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult();
return testServer;
}
diff --git a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
index 03ab56d1f..0ade345a1 100644
--- a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
@@ -1,12 +1,10 @@
using System.IO;
using System.Reflection;
-using System.Text.Json;
using System.Threading.Tasks;
-using MediaBrowser.Model.Branding;
using Xunit;
using Xunit.Abstractions;
-namespace Jellyfin.Api.Tests
+namespace Jellyfin.Server.Integration.Tests
{
public sealed class OpenApiSpecTests : IClassFixture<JellyfinApplicationFactory>
{
diff --git a/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
new file mode 100644
index 000000000..0a463cfa3
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Reflection;
+using Emby.Server.Implementations;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Integration.Tests
+{
+ /// <summary>
+ /// Implementation of the abstract <see cref="ApplicationHost" /> class.
+ /// </summary>
+ public class TestAppHost : CoreAppHost
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TestAppHost" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
+ /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
+ /// <param name="startup">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
+ /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
+ /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
+ public TestAppHost(
+ IServerApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IStartupOptions options,
+ IConfiguration startup,
+ IFileSystem fileSystem,
+ IServiceCollection collection)
+ : base(
+ applicationPaths,
+ loggerFactory,
+ options,
+ startup,
+ fileSystem,
+ collection)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
+ {
+ foreach (var a in base.GetAssembliesWithPartsInternal())
+ {
+ yield return a;
+ }
+
+ yield return typeof(TestPlugin).Assembly;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/TestPage.html b/tests/Jellyfin.Server.Integration.Tests/TestPage.html
new file mode 100644
index 000000000..8037af8a6
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/TestPage.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>TestPlugin</title>
+</head>
+<body>
+ <h1>This is a Test Page.</h1>
+</body>
+</html>
diff --git a/tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs b/tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs
new file mode 100644
index 000000000..1d67ac487
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs
@@ -0,0 +1,43 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace Jellyfin.Server.Integration.Tests
+{
+ public class TestPlugin : BasePlugin<BasePluginConfiguration>, IHasWebPages
+ {
+ public TestPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ public static TestPlugin? Instance { get; private set; }
+
+ public override Guid Id => new Guid("2d350a13-0bf7-4b61-859c-d5e601b5facf");
+
+ public override string Name => nameof(TestPlugin);
+
+ public override string Description => "Server test Plugin.";
+
+ public IEnumerable<PluginPageInfo> GetPages()
+ {
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = GetType().Namespace + ".TestPage.html"
+ };
+
+ yield return new PluginPageInfo
+ {
+ Name = "BrokenPage",
+ EmbeddedResourcePath = GetType().Namespace + ".foobar"
+ };
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/TestPluginWithoutPages.cs b/tests/Jellyfin.Server.Integration.Tests/TestPluginWithoutPages.cs
new file mode 100644
index 000000000..ac10c4784
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/TestPluginWithoutPages.cs
@@ -0,0 +1,27 @@
+#pragma warning disable CS1591
+
+using System;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace Jellyfin.Server.Integration.Tests
+{
+ public class TestPluginWithoutPages : BasePlugin<BasePluginConfiguration>
+ {
+ public TestPluginWithoutPages(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ public static TestPluginWithoutPages? Instance { get; private set; }
+
+ public override Guid Id => new Guid("ae95cbe6-bd3d-4d73-8596-490db334611e");
+
+ public override string Name => nameof(TestPluginWithoutPages);
+
+ public override string Description => "Server test Plugin without web pages.";
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/WebSocketTests.cs b/tests/Jellyfin.Server.Integration.Tests/WebSocketTests.cs
new file mode 100644
index 000000000..ffdc04eba
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/WebSocketTests.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests
+{
+ public sealed class WebSocketTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+
+ public WebSocketTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task WebSocket_Unauthenticated_ThrowsInvalidOperationException()
+ {
+ var server = _factory.Server;
+ var client = server.CreateWebSocketClient();
+
+ await Assert.ThrowsAsync<InvalidOperationException>(
+ () => client.ConnectAsync(
+ new UriBuilder(server.BaseAddress)
+ {
+ Scheme = "ws",
+ Path = "websocket"
+ }.Uri, CancellationToken.None));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/xunit.runner.json b/tests/Jellyfin.Server.Integration.Tests/xunit.runner.json
new file mode 100644
index 000000000..809e880c7
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/xunit.runner.json
@@ -0,0 +1,4 @@
+{
+ "parallelizeAssembly": false,
+ "parallelizeTestCollections": false
+}
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
new file mode 100644
index 000000000..c8e72c10d
--- /dev/null
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="Moq" Version="4.16.0" />
+ </ItemGroup>
+
+ <!-- Code Analyzers -->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
index 6c3fd0ee1..146b16cf9 100644
--- a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs
+++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
@@ -1,4 +1,3 @@
-using System;
using System.Globalization;
using System.Text;
using Jellyfin.Networking.Configuration;
@@ -10,7 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
-namespace Jellyfin.Api.Tests
+namespace Jellyfin.Server.Tests
{
public class ParseNetworkTests
{
@@ -37,28 +36,28 @@ namespace Jellyfin.Api.Tests
EnableIPV6 = ip6
};
- var result = match + ',';
+ var result = match + ",";
ForwardedHeadersOptions options = new ForwardedHeadersOptions();
// Need this here as ::1 and 127.0.0.1 are in them by default.
options.KnownProxies.Clear();
options.KnownNetworks.Clear();
- ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList.Split(","), options);
+ ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList.Split(','), options);
var sb = new StringBuilder();
foreach (var item in options.KnownProxies)
{
- sb.Append(item);
- sb.Append(',');
+ sb.Append(item)
+ .Append(',');
}
foreach (var item in options.KnownNetworks)
{
- sb.Append(item.Prefix);
- sb.Append('/');
- sb.Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture));
- sb.Append(',');
+ sb.Append(item.Prefix)
+ .Append('/')
+ .Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture))
+ .Append(',');
}
Assert.Equal(sb.ToString(), result);
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
new file mode 100644
index 000000000..419afb2dc
--- /dev/null
+++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Server.Middleware;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Jellyfin.Server.Tests
+{
+ public static class UrlDecodeQueryFeatureTests
+ {
+ [Theory]
+ [InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded
+ [InlineData("random+test", "random test")] // encoded
+ [InlineData("random%20test", "random test")] // encoded
+ [InlineData("++", " ")] // encoded
+ public static void EmptyValueTest(string query, string key)
+ {
+ var dict = new Dictionary<string, StringValues>
+ {
+ { query, StringValues.Empty }
+ };
+ var test = new UrlDecodeQueryFeature(new QueryFeature(new QueryCollection(dict)));
+ Assert.Single(test.Query);
+ var (k, v) = test.Query.First();
+ Assert.Equal(key, k);
+ Assert.Empty(v);
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index aed3e8ac5..0a04a5c54 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -5,6 +5,8 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -14,16 +16,15 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
- <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.2" />
+ <PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -34,8 +35,4 @@
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs
new file mode 100644
index 000000000..357d61c0b
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs
@@ -0,0 +1,65 @@
+using System.Linq;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.System;
+using MediaBrowser.XbmcMetadata.Savers;
+using Xunit;
+
+namespace Jellyfin.XbmcMetadata.Tests.Location
+{
+ public class MovieNfoLocationTests
+ {
+ [Fact]
+ public static void Movie_MixedFolder_Success()
+ {
+ var movie = new Movie() { Path = "/media/movies/Avengers Endgame.mp4", IsInMixedFolder = true };
+
+ var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
+ Assert.Single(paths);
+ Assert.Contains("/media/movies/Avengers Endgame.nfo", paths);
+ }
+
+ [Fact]
+ public static void Movie_SeparateFolder_Success()
+ {
+ var movie = new Movie() { Path = "/media/movies/Avengers Endgame/Avengers Endgame.mp4" };
+ var path1 = "/media/movies/Avengers Endgame/Avengers Endgame.nfo";
+ var path2 = "/media/movies/Avengers Endgame/movie.nfo";
+
+ // uses ContainingFolderPath which uses Operating system specific paths
+ if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
+ {
+ movie.Path = movie.Path.Replace('/', '\\');
+ path1 = path1.Replace('/', '\\');
+ path2 = path2.Replace('/', '\\');
+ }
+
+ var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
+ Assert.Equal(2, paths.Length);
+ Assert.Contains(path1, paths);
+ Assert.Contains(path2, paths);
+ }
+
+ [Fact]
+ public void Movie_DVD_Success()
+ {
+ var movie = new Movie() { Path = "/media/movies/Avengers Endgame", VideoType = VideoType.Dvd };
+ var path1 = "/media/movies/Avengers Endgame/Avengers Endgame.nfo";
+ var path2 = "/media/movies/Avengers Endgame/VIDEO_TS/VIDEO_TS.nfo";
+
+ // uses ContainingFolderPath which uses Operating system specific paths
+ if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
+ {
+ movie.Path = movie.Path.Replace('/', '\\');
+ path1 = path1.Replace('/', '\\');
+ path2 = path2.Replace('/', '\\');
+ }
+
+ var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
+ Assert.Equal(2, paths.Length);
+ Assert.Contains(path1, paths);
+ Assert.Contains(path2, paths);
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index d0cd8b287..3e726f23d 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -13,8 +14,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
-#pragma warning disable CA5369
-
namespace Jellyfin.XbmcMetadata.Tests.Parsers
{
public class EpisodeNfoProviderTests
@@ -34,11 +33,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var config = new Mock<IConfigurationManager>();
config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
.Returns(new XbmcMetadataOptions());
- _parser = new EpisodeNfoParser(new NullLogger<EpisodeNfoParser>(), config.Object, providerManager.Object);
+ var user = new Mock<IUserManager>();
+ var userData = new Mock<IUserDataManager>();
+ var directoryService = new Mock<IDirectoryService>();
+
+ _parser = new EpisodeNfoParser(
+ new NullLogger<EpisodeNfoParser>(),
+ config.Object,
+ providerManager.Object,
+ user.Object,
+ userData.Object,
+ directoryService.Object);
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<Episode>()
{
@@ -62,6 +71,10 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(2017, item.ProductionYear);
Assert.Single(item.Studios);
Assert.Contains("Starz", item.Studios);
+ Assert.Equal(1, item.IndexNumberEnd);
+ Assert.Equal(2, item.AirsAfterSeasonNumber);
+ Assert.Equal(3, item.AirsBeforeSeasonNumber);
+ Assert.Equal(1, item.AirsBeforeEpisodeNumber);
Assert.Equal("tt5017734", item.ProviderIds[MetadataProvider.Imdb.ToString()]);
Assert.Equal("1276153", item.ProviderIds[MetadataProvider.Tmdb.ToString()]);
@@ -90,6 +103,26 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[Fact]
+ public void Fetch_Valid_MultiEpisode_Success()
+ {
+ var result = new MetadataResult<Episode>()
+ {
+ Item = new Episode()
+ };
+
+ _parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
+
+ var item = result.Item;
+ Assert.Equal("Rising (1)", item.Name);
+ Assert.Equal(1, item.IndexNumber);
+ Assert.Equal(2, item.IndexNumberEnd);
+ Assert.Equal(1, item.ParentIndexNumber);
+ Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
+ Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
+ Assert.Equal(2004, item.ProductionYear);
+ }
+
+ [Fact]
public void Fetch_WithNullItem_ThrowsArgumentException()
{
var result = new MetadataResult<Episode>();
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index 2f7ee65d5..30a48857a 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -1,12 +1,17 @@
using System;
using System.Linq;
using System.Threading;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.System;
using MediaBrowser.Providers.Plugins.Tmdb.Movies;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;
@@ -18,9 +23,14 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
public class MovieNfoParserTests
{
private readonly MovieNfoParser _parser;
+ private readonly IUserDataManager _userDataManager;
+ private readonly User _testUser;
+ private readonly FileSystemMetadata _localImageFileMetadata;
public MovieNfoParserTests()
{
+ _testUser = new User("Test User", "Auth provider", "Reset provider");
+
var providerManager = new Mock<IProviderManager>();
var tmdbExternalId = new TmdbMovieExternalId();
@@ -29,22 +39,53 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
.Returns(new[] { externalIdInfo });
- var config = new Mock<IConfigurationManager>();
- config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
- .Returns(new XbmcMetadataOptions());
- _parser = new MovieNfoParser(new NullLogger<MovieNfoParser>(), config.Object, providerManager.Object);
+ var nfoConfig = new XbmcMetadataOptions()
+ {
+ UserId = "F38E6443-090B-4F7A-BD12-9CFF5020F7BC"
+ };
+ var configManager = new Mock<IConfigurationManager>();
+ configManager.Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(nfoConfig);
+
+ var user = new Mock<IUserManager>();
+ user.Setup(x => x.GetUserById(It.IsAny<Guid>()))
+ .Returns(_testUser);
+
+ var userData = new Mock<IUserDataManager>();
+ userData.Setup(x => x.GetUserData(_testUser, It.IsAny<BaseItem>()))
+ .Returns(new UserItemData());
+
+ var directoryService = new Mock<IDirectoryService>();
+ _localImageFileMetadata = new FileSystemMetadata()
+ {
+ Exists = true,
+ FullName = MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows ?
+ "C:\\media\\movies\\Justice League (2017).jpg"
+ : "/media/movies/Justice League (2017).jpg"
+ };
+ directoryService.Setup(x => x.GetFile(_localImageFileMetadata.FullName))
+ .Returns(_localImageFileMetadata);
+
+ _userDataManager = userData.Object;
+ _parser = new MovieNfoParser(
+ new NullLogger<MovieNfoParser>(),
+ configManager.Object,
+ providerManager.Object,
+ user.Object,
+ userData.Object,
+ directoryService.Object);
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<Video>()
{
- Item = new Video()
+ Item = new Movie()
};
_parser.Fetch(result, "Test Data/Justice League.nfo", CancellationToken.None);
- var item = result.Item;
+ var item = (Movie)result.Item;
Assert.Equal("Justice League", item.OriginalTitle);
Assert.Equal("Justice for all.", item.Tagline);
@@ -58,22 +99,31 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Contains("Sci-Fi", item.Genres);
Assert.Equal(new DateTime(2017, 11, 15), item.PremiereDate);
+ Assert.Equal(new DateTime(2017, 11, 16), item.EndDate);
Assert.Single(item.Studios);
Assert.Contains("DC Comics", item.Studios);
Assert.Equal("1.777778", item.AspectRatio);
+ Assert.Equal(Video3DFormat.HalfSideBySide, item.Video3DFormat);
Assert.Equal(1920, item.Width);
Assert.Equal(1080, item.Height);
Assert.Equal(new TimeSpan(0, 0, 6268).Ticks, item.RunTimeTicks);
Assert.True(item.HasSubtitles);
+ Assert.Equal(7.6f, item.CriticRating);
+ Assert.Equal("8.7", item.CustomRating);
+ Assert.Equal("en", item.PreferredMetadataLanguage);
+ Assert.Equal("us", item.PreferredMetadataCountryCode);
+ Assert.Single(item.RemoteTrailers);
+ Assert.Equal("https://www.youtube.com/watch?v=dQw4w9WgXcQ", item.RemoteTrailers[0].Url);
- Assert.Equal(19, result.People.Count);
+ Assert.Equal(20, result.People.Count);
var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray();
- Assert.Equal(2, writers.Length);
+ Assert.Equal(3, writers.Length);
var writerNames = writers.Select(x => x.Name);
Assert.Contains("Jerry Siegel", writerNames);
Assert.Contains("Joe Shuster", writerNames);
+ Assert.Contains("Test", writerNames);
var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray();
Assert.Single(directors);
@@ -94,6 +144,82 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal("Test Lyricist", lyricist!.Name);
Assert.Equal(new DateTime(2019, 8, 6, 9, 1, 18), item.DateCreated);
+
+ // userData
+ var userData = _userDataManager.GetUserData(_testUser, item);
+ Assert.Equal(2, userData.PlayCount);
+ Assert.True(userData.Played);
+ Assert.Equal(new DateTime(2021, 02, 11, 07, 47, 23), userData.LastPlayedDate);
+
+ // Movie set
+ Assert.Equal("702342", item.ProviderIds[MetadataProvider.TmdbCollection.ToString()]);
+ Assert.Equal("Justice League Collection", item.CollectionName);
+
+ // Images
+ Assert.Equal(7, result.RemoteImages.Count);
+
+ var posters = result.RemoteImages.Where(x => x.type == ImageType.Primary).ToList();
+ Assert.Single(posters);
+ Assert.Equal("http://image.tmdb.org/t/p/original/9rtrRGeRnL0JKtu9IMBWsmlmmZz.jpg", posters[0].url);
+
+ var logos = result.RemoteImages.Where(x => x.type == ImageType.Logo).ToList();
+ Assert.Single(logos);
+ Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png", logos[0].url);
+
+ var banners = result.RemoteImages.Where(x => x.type == ImageType.Banner).ToList();
+ Assert.Single(banners);
+ Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-586017e95adbd.jpg", banners[0].url);
+
+ var thumbs = result.RemoteImages.Where(x => x.type == ImageType.Thumb).ToList();
+ Assert.Single(thumbs);
+ Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585fb155c3743.jpg", thumbs[0].url);
+
+ var art = result.RemoteImages.Where(x => x.type == ImageType.Art).ToList();
+ Assert.Single(art);
+ Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/hdmovieclearart/justice-league-5865c23193041.png", art[0].url);
+
+ var discArt = result.RemoteImages.Where(x => x.type == ImageType.Disc).ToList();
+ Assert.Single(discArt);
+ Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a3af26360617.png", discArt[0].url);
+
+ var backdrop = result.RemoteImages.Where(x => x.type == ImageType.Backdrop).ToList();
+ Assert.Single(backdrop);
+ Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg", backdrop[0].url);
+
+ // Local Image - contains only one item depending on operating system
+ Assert.Single(result.Images);
+ Assert.Equal(_localImageFileMetadata.Name, result.Images[0].FileInfo.Name);
+ }
+
+ [Theory]
+ [InlineData("Test Data/Tmdb.nfo", "Tmdb", "30287")]
+ [InlineData("Test Data/Imdb.nfo", "Imdb", "tt0944947")]
+ public void Parse_UrlFile_Success(string path, string provider, string id)
+ {
+ var result = new MetadataResult<Video>()
+ {
+ Item = new Movie()
+ };
+
+ _parser.Fetch(result, path, CancellationToken.None);
+ var item = (Movie)result.Item;
+
+ Assert.Equal(id, item.ProviderIds[provider]);
+ }
+
+ [Fact]
+ public void Parse_RadarrUrlFile_Success()
+ {
+ var result = new MetadataResult<Video>()
+ {
+ Item = new Movie()
+ };
+
+ _parser.Fetch(result, "Test Data/Radarr.nfo", CancellationToken.None);
+ var item = (Movie)result.Item;
+
+ Assert.Equal("583689", item.ProviderIds[MetadataProvider.Tmdb.ToString()]);
+ Assert.Equal("tt4154796", item.ProviderIds[MetadataProvider.Imdb.ToString()]);
}
[Fact]
@@ -109,7 +235,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
{
var result = new MetadataResult<Video>()
{
- Item = new Video()
+ Item = new Movie()
};
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
index 1fe56cadd..eea8cb50a 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
@@ -1,16 +1,13 @@
-#pragma warning disable CA5369
-
-using System;
-using System.Linq;
+using System;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.Music;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
@@ -35,11 +32,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var config = new Mock<IConfigurationManager>();
config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
.Returns(new XbmcMetadataOptions());
- _parser = new BaseNfoParser<MusicAlbum>(new NullLogger<BaseNfoParser<MusicAlbum>>(), config.Object, providerManager.Object);
+ var user = new Mock<IUserManager>();
+ var userData = new Mock<IUserDataManager>();
+ var directoryService = new Mock<IDirectoryService>();
+
+ _parser = new BaseNfoParser<MusicAlbum>(
+ new NullLogger<BaseNfoParser<MusicAlbum>>(),
+ config.Object,
+ providerManager.Object,
+ user.Object,
+ userData.Object,
+ directoryService.Object);
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<MusicAlbum>()
{
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
index 4869cf088..8ca3dd96e 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
@@ -1,8 +1,8 @@
using System;
-using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -32,11 +32,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var config = new Mock<IConfigurationManager>();
config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
.Returns(new XbmcMetadataOptions());
- _parser = new BaseNfoParser<MusicArtist>(new NullLogger<BaseNfoParser<MusicArtist>>(), config.Object, providerManager.Object);
+ var user = new Mock<IUserManager>();
+ var userData = new Mock<IUserDataManager>();
+ var directoryService = new Mock<IDirectoryService>();
+
+ _parser = new BaseNfoParser<MusicArtist>(
+ new NullLogger<BaseNfoParser<MusicArtist>>(),
+ config.Object,
+ providerManager.Object,
+ user.Object,
+ userData.Object,
+ directoryService.Object);
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<MusicArtist>()
{
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicVideoNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicVideoNfoParserTests.cs
new file mode 100644
index 000000000..bf887cab1
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicVideoNfoParserTests.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.XbmcMetadata.Parsers;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.XbmcMetadata.Tests.Parsers
+{
+ public class MusicVideoNfoParserTests
+ {
+ private readonly MovieNfoParser _parser;
+
+ public MusicVideoNfoParserTests()
+ {
+ var providerManager = new Mock<IProviderManager>();
+ providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
+ .Returns(Enumerable.Empty<ExternalIdInfo>());
+ var config = new Mock<IConfigurationManager>();
+ config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new XbmcMetadataOptions());
+
+ var user = new Mock<IUserManager>();
+ var userData = new Mock<IUserDataManager>();
+ var directoryService = new Mock<IDirectoryService>();
+
+ _parser = new MovieNfoParser(
+ new NullLogger<BaseNfoParser<MusicVideo>>(),
+ config.Object,
+ providerManager.Object,
+ user.Object,
+ userData.Object,
+ directoryService.Object);
+ }
+
+ [Fact]
+ public void Fetch_Valid_Succes()
+ {
+ var result = new MetadataResult<Video>()
+ {
+ Item = new MusicVideo()
+ };
+
+ _parser.Fetch(result, "Test Data/Dancing Queen.nfo", CancellationToken.None);
+ var item = (MusicVideo)result.Item;
+
+ Assert.Equal("Dancing Queen", item.Name);
+ Assert.Single(item.Artists);
+ Assert.Contains("ABBA", item.Artists);
+ Assert.Equal("Arrival", item.Album);
+ }
+
+ [Fact]
+ public void Fetch_WithNullItem_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Video>();
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, "Test Data/Dancing Queen.nfo", CancellationToken.None));
+ }
+
+ [Fact]
+ public void Fetch_NullResult_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Video>()
+ {
+ Item = new MusicVideo()
+ };
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
index 68b7239d2..31110dbd7 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
@@ -1,10 +1,9 @@
-#pragma warning disable CA5369
-
-using System;
+using System;
using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -28,11 +27,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var config = new Mock<IConfigurationManager>();
config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
.Returns(new XbmcMetadataOptions());
- _parser = new SeasonNfoParser(new NullLogger<SeasonNfoParser>(), config.Object, providerManager.Object);
+ var user = new Mock<IUserManager>();
+ var userData = new Mock<IUserDataManager>();
+ var directoryService = new Mock<IDirectoryService>();
+
+ _parser = new SeasonNfoParser(
+ new NullLogger<SeasonNfoParser>(),
+ config.Object,
+ providerManager.Object,
+ user.Object,
+ userData.Object,
+ directoryService.Object);
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<Season>()
{
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
index 9e535182e..bdedae205 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -26,11 +27,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var config = new Mock<IConfigurationManager>();
config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
.Returns(new XbmcMetadataOptions());
- _parser = new SeriesNfoParser(new NullLogger<SeriesNfoParser>(), config.Object, providerManager.Object);
+ var user = new Mock<IUserManager>();
+ var userData = new Mock<IUserDataManager>();
+ var directoryService = new Mock<IDirectoryService>();
+
+ _parser = new SeriesNfoParser(new NullLogger<SeriesNfoParser>(), config.Object, providerManager.Object, user.Object, userData.Object, directoryService.Object);
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<Series>()
{
@@ -45,6 +50,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(0, item.RunTimeTicks);
Assert.Equal("46639", item.ProviderIds[MetadataProvider.Tmdb.ToString()]);
Assert.Equal("253573", item.ProviderIds[MetadataProvider.Tvdb.ToString()]);
+ Assert.Equal("tt11111", item.ProviderIds[MetadataProvider.Imdb.ToString()]);
Assert.Equal(3, item.Genres.Length);
Assert.Contains("Drama", item.Genres);
@@ -54,6 +60,10 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(new DateTime(2017, 4, 30), item.PremiereDate);
Assert.Single(item.Studios);
Assert.Contains("Starz", item.Studios);
+ Assert.Equal("9 PM", item.AirTime);
+ Assert.Single(item.AirDays);
+ Assert.Contains(DayOfWeek.Friday, item.AirDays);
+ Assert.Equal(SeriesStatus.Ended, item.Status);
Assert.Equal(6, result.People.Count);
@@ -69,6 +79,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(new DateTime(2017, 10, 7, 14, 25, 47), item.DateCreated);
}
+ [Theory]
+ [InlineData("Test Data/Tvdb.nfo", "Tvdb", "121361")]
+ public void Parse_UrlFile_Success(string path, string provider, string id)
+ {
+ var result = new MetadataResult<Series>()
+ {
+ Item = new Series()
+ };
+
+ _parser.Fetch(result, path, CancellationToken.None);
+ var item = (Series)result.Item;
+
+ Assert.Equal(id, item.ProviderIds[provider]);
+ }
+
[Fact]
public void Fetch_WithNullItem_ThrowsArgumentException()
{
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo
index b9f31f2f6..5bf7e08eb 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo
@@ -126,7 +126,7 @@
<episodeguide>
<url cache="tmdb-46639-en.json">http://api.themoviedb.org/3/tv/46639?api_key=6a5be4999abf74eba1f9a8311294c267&amp;language=en</url>
</episodeguide>
- <id>46639</id>
+ <id IMDB="tt11111" TMDB="46639">253573</id>
<uniqueid type="tmdb" default="true">46639</uniqueid>
<uniqueid type="tvdb">253573</uniqueid>
<genre>Drama</genre>
@@ -134,7 +134,9 @@
<genre>Sci-Fi &amp; Fantasy</genre>
<premiered>2017-04-30</premiered>
<year>2017</year>
- <status></status>
+ <status>ended</status>
+ <airs_time>9 PM</airs_time>
+ <airs_dayofweek>Friday</airs_dayofweek>
<code></code>
<aired></aired>
<studio>Starz</studio>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Dancing Queen.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Dancing Queen.nfo
new file mode 100644
index 000000000..29f19e1a0
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Dancing Queen.nfo
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<musicvideo>
+ <title>Dancing Queen</title>
+ <userrating>0</userrating>
+ <top250>0</top250>
+ <track>3</track>
+ <album>Arrival</album>
+ <outline></outline>
+ <plot>Dancing Queen est un des tubes emblématiques de l&apos;ère disco produits par le groupe suédois ABBA en 1976. Ce tube connaît un regain de popularité en 1994 lors de la sortie de Priscilla, folle du désert, et fait « presque » partie de la distribution du film Muriel.&#x0A;Le groupe a également enregistré une version espagnole de ce titre, La reina del baile, pour le marché d&apos;Amérique latine. On peut retrouver ces versions en espagnol des succès de ABBA sur l&apos;abum Oro. Le 18 juin 1976, ABBA a interprété cette chanson lors d&apos;un spectacle télévisé organisé en l&apos;honneur du roi Charles XVI Gustave de Suède, qui venait de se marier. Le titre sera repris en 2011 par Glee dans la saison 2, épisode 20.</plot>
+ <tagline></tagline>
+ <runtime>2</runtime>
+ <thumb preview="https://www.theaudiodb.com/images/media/album/thumb/arrival-4ee244732bbde.jpg/preview">https://www.theaudiodb.com/images/media/album/thumb/arrival-4ee244732bbde.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/music/d87e52c5-bb8d-4da8-b941-9f4928627dc8/albumcover/arrival-548ab7a698b49.jpg">https://assets.fanart.tv/fanart/music/d87e52c5-bb8d-4da8-b941-9f4928627dc8/albumcover/arrival-548ab7a698b49.jpg</thumb>
+ <mpaa></mpaa>
+ <playcount>0</playcount>
+ <lastplayed></lastplayed>
+ <id></id>
+ <genre>Pop</genre>
+ <director>John Smith</director>
+ <premiered>1976-01-01</premiered>
+ <year>1976</year>
+ <status></status>
+ <code></code>
+ <aired></aired>
+ <studio>Studio 54</studio>
+ <trailer></trailer>
+ <fileinfo>
+ <streamdetails>
+ <video>
+ <codec>hevc</codec>
+ <aspect>1.792230</aspect>
+ <width>716</width>
+ <height>568</height>
+ <durationinseconds>143</durationinseconds>
+ <stereomode></stereomode>
+ </video>
+ <audio>
+ <codec>ac3</codec>
+ <language>eng</language>
+ <channels>2</channels>
+ </audio>
+ </streamdetails>
+ </fileinfo>
+ <artist>ABBA</artist>
+ <resume>
+ <position>0.000000</position>
+ <total>0.000000</total>
+ </resume>
+ <dateadded>2018-09-10 09:46:06</dateadded>
+</musicvideo>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Imdb.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Imdb.nfo
new file mode 100644
index 000000000..e30a1c660
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Imdb.nfo
@@ -0,0 +1 @@
+https://www.imdb.com/title/tt0944947/
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo
index 18b44d944..4e8c79dca 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo
@@ -24,12 +24,20 @@
<votes>119873</votes>
</rating>
</ratings>
+ <criticrating>7.6</criticrating>
+ <language>en</language>
+ <countrycode>us</countrycode>
+ <customrating>8.7</customrating>
+ <aspectratio>1.777778</aspectratio>
<userrating>0</userrating>
<top250>0</top250>
<outline>Fueled by his restored faith in humanity and inspired by Superman&apos;s selfless act, Bruce Wayne enlists the help of his new-found ally, Diana Prince, to face an even greater enemy.</outline>
<plot>Fueled by his restored faith in humanity and inspired by Superman&apos;s selfless act, Bruce Wayne enlists the help of his newfound ally, Diana Prince, to face an even greater enemy. Together, Batman and Wonder Woman work quickly to find and recruit a team of meta-humans to stand against this newly awakened threat. But despite the formation of this unprecedented league of heroes-Batman, Wonder Woman, Aquaman, Cyborg and The Flash-it may already be too late to save the planet from an assault of catastrophic proportions.</plot>
<tagline>Justice for all.</tagline>
<runtime>120</runtime>
+ <playcount>2</playcount>
+ <watched>true</watched>
+ <lastplayed>2021-02-11 07:47:23</lastplayed>
<tmdbId>141052</tmdbId>
<thumb aspect="set.poster" preview="https://assets.fanart.tv/preview/movies/468551/movieposter/justice-league-collection-5c24ea65591d3.jpg">https://assets.fanart.tv/fanart/movies/468551/movieposter/justice-league-collection-5c24ea65591d3.jpg</thumb>
<thumb aspect="set.poster" preview="https://assets.fanart.tv/preview/movies/468551/movieposter/justice-league-collection-5c24ea65591d3.jpg">https://assets.fanart.tv/fanart/movies/468551/movieposter/justice-league-collection-5c24ea65591d3.jpg</thumb>
@@ -51,6 +59,8 @@
<thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/exLtrlI7JjKcfQVTccI7XdQRFMz.jpg">http://image.tmdb.org/t/p/original/exLtrlI7JjKcfQVTccI7XdQRFMz.jpg</thumb>
<thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/paLcue01KpfQftorfjKqqD4qvlL.jpg">http://image.tmdb.org/t/p/original/paLcue01KpfQftorfjKqqD4qvlL.jpg</thumb>
<thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/yVDIfiKIsCbdFcgLXW34bAsnQvy.jpg">http://image.tmdb.org/t/p/original/yVDIfiKIsCbdFcgLXW34bAsnQvy.jpg</thumb>
+ <thumb aspect="poster">C:\media\movies\Justice League (2017).jpg</thumb>
+ <thumb aspect="poster">/media/movies/Justice League (2017).jpg</thumb>
<thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png</thumb>
<thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png</thumb>
<thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57b476a831d74.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57b476a831d74.png</thumb>
@@ -72,8 +82,8 @@
<thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a0b913c233be.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a0b913c233be.png</thumb>
<thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png</thumb>
<thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-59dc595362ef1.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-59dc595362ef1.png</thumb>
+ <thumb aspect="fanart">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg</thumb>
<fanart>
- <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg</thumb>
<thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg</thumb>
<thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg</thumb>
<thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg</thumb>
@@ -82,10 +92,9 @@
<thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a119394ea362.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a119394ea362.jpg</thumb>
</fanart>
<mpaa>Australia:M</mpaa>
- <playcount>0</playcount>
- <lastplayed></lastplayed>
<id>tt0974015</id>
- <uniqueid type="imdb" default="true">tt0974015</uniqueid>
+ <uniqueid type="imdb">tt0974015</uniqueid>
+ <uniqueid type="tmdb">141052</uniqueid>
<genre>Action</genre>
<genre>Adventure</genre>
<genre>Fantasy</genre>
@@ -93,20 +102,22 @@
<country>USA</country>
<country>Canada</country>
<country>UK</country>
- <set>
+ <set tmdbcolid="702342">
<name>Justice League Collection</name>
<overview>Based on the DC Comics superhero team</overview>
</set>
<credits>Jerry Siegel</credits>
<credits>Joe Shuster</credits>
- <director>Zack Snyder</director>
+ <director>Zack Snyder,</director>
+ <writer>Test</writer>
<premiered>2017-11-15</premiered>
+ <enddate>2017-11-16</enddate>
<year>2017</year>
<status></status>
<code></code>
<aired></aired>
<studio>DC Comics</studio>
- <trailer></trailer>
+ <trailer>plugin://plugin.video.youtube/?action=play_video&amp;videoid=dQw4w9WgXcQ</trailer>
<fileinfo>
<streamdetails>
<video>
@@ -116,6 +127,7 @@
<height>1080</height>
<durationinseconds>6268</durationinseconds>
<stereomode></stereomode>
+ <format3d>HSBS</format3d>
</video>
<audio>
<codec>truehd</codec>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Radarr.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Radarr.nfo
new file mode 100644
index 000000000..43da4881c
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Radarr.nfo
@@ -0,0 +1,2 @@
+https://www.themoviedb.org/movie/583689
+https://www.imdb.com/title/tt4154796
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo
new file mode 100644
index 000000000..56250c09a
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo
@@ -0,0 +1,20 @@
+<episodedetails>
+ <title>Rising (1)</title>
+ <season>1</season>
+ <episode>1</episode>
+ <aired>2004-07-16</aired>
+ <plot>A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.</plot>
+ <thumb>https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg</thumb>
+ <watched>false</watched>
+ <rating>8.0</rating>
+</episodedetails>
+<episodedetails>
+ <title>Rising (2)</title>
+ <season>1</season>
+ <episode>2</episode>
+ <aired>2004-07-16</aired>
+ <plot>Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.</plot>
+ <thumb>https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg</thumb>
+ <watched>false</watched>
+ <rating>7.9</rating>
+</episodedetails>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo
index 14feffcba..cd275e4cf 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo
@@ -14,6 +14,10 @@
<episode>1</episode>
<displayseason>-1</displayseason>
<displayepisode>-1</displayepisode>
+ <episodenumberend>1</episodenumberend>
+ <airsbefore_episode>1</airsbefore_episode>
+ <airsafter_season>2</airsafter_season>
+ <airsbefore_season>3</airsbefore_season>
<outline></outline>
<plot>When Shadow Moon is released from prison early after the death of his wife, he meets Mr. Wednesday and is recruited as his bodyguard. Shadow discovers that this may be more than he bargained for.</plot>
<tagline></tagline>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Tmdb.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Tmdb.nfo
new file mode 100644
index 000000000..15af71852
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Tmdb.nfo
@@ -0,0 +1 @@
+https://www.themoviedb.org/movie/30287-fallo
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Tvdb.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Tvdb.nfo
new file mode 100644
index 000000000..9de69f8e1
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Tvdb.nfo
@@ -0,0 +1 @@
+https://www.thetvdb.com/?tab=series&id=121361