aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml4
-rw-r--r--.ci/azure-pipelines-test.yml2
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Emby.Dlna/ContentDirectory/StubType.cs1
-rw-r--r--Emby.Dlna/DlnaManager.cs14
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj1
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs17
-rw-r--r--Emby.Dlna/PlayTo/Device.cs55
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs4
-rw-r--r--Emby.Dlna/PlayTo/SsdpHttpClient.cs16
-rw-r--r--Emby.Dlna/PlayTo/TransportCommands.cs6
-rw-r--r--Emby.Dlna/PlayTo/TransportState.cs1
-rw-r--r--Emby.Dlna/Profiles/DefaultProfile.cs3
-rw-r--r--Emby.Dlna/Ssdp/DeviceDiscovery.cs2
-rw-r--r--Emby.Drawing/Emby.Drawing.csproj1
-rw-r--r--Emby.Drawing/ImageProcessor.cs7
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs2
-rw-r--r--Emby.Naming/Emby.Naming.csproj1
-rw-r--r--Emby.Naming/Video/CleanStringParser.cs11
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs15
-rw-r--r--Emby.Naming/Video/VideoResolver.cs3
-rw-r--r--Emby.Notifications/Emby.Notifications.csproj1
-rw-r--r--Emby.Photos/Emby.Photos.csproj1
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs3
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs41
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs2
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs17
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs6
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs92
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs2
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj4
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs18
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs2
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs12
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs91
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs2
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs63
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs208
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs19
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs256
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json2
-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/gl.json74
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json4
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs2
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs2
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs101
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs2
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs1
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs2
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs24
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs8
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs47
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs48
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs27
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs23
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs32
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs90
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs2
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs10
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs15
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs21
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs20
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs3
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs5
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs6
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs8
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs16
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs9
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs13
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs12
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs16
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj5
-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/TranscodingThrottler.cs2
-rw-r--r--Jellyfin.Data/DayOfWeekHelper.cs60
-rw-r--r--Jellyfin.Data/Entities/AccessSchedule.cs29
-rw-r--r--Jellyfin.Data/Entities/ActivityLog.cs21
-rw-r--r--Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs20
-rw-r--r--Jellyfin.Data/Entities/DisplayPreferences.cs14
-rw-r--r--Jellyfin.Data/Entities/Group.cs11
-rw-r--r--Jellyfin.Data/Entities/HomeSection.cs1
-rw-r--r--Jellyfin.Data/Entities/ImageInfo.cs11
-rw-r--r--Jellyfin.Data/Entities/ItemDisplayPreferences.cs9
-rw-r--r--Jellyfin.Data/Entities/Libraries/Artwork.cs16
-rw-r--r--Jellyfin.Data/Entities/Libraries/Book.cs3
-rw-r--r--Jellyfin.Data/Entities/Libraries/BookMetadata.cs23
-rw-r--r--Jellyfin.Data/Entities/Libraries/Chapter.cs23
-rw-r--r--Jellyfin.Data/Entities/Libraries/Collection.cs3
-rw-r--r--Jellyfin.Data/Entities/Libraries/CollectionItem.cs40
-rw-r--r--Jellyfin.Data/Entities/Libraries/Company.cs17
-rw-r--r--Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs25
-rw-r--r--Jellyfin.Data/Entities/Libraries/CustomItem.cs3
-rw-r--r--Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs21
-rw-r--r--Jellyfin.Data/Entities/Libraries/Episode.cs22
-rw-r--r--Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs26
-rw-r--r--Jellyfin.Data/Entities/Libraries/Genre.cs27
-rw-r--r--Jellyfin.Data/Entities/Libraries/ItemMetadata.cs16
-rw-r--r--Jellyfin.Data/Entities/Libraries/Library.cs22
-rw-r--r--Jellyfin.Data/Entities/Libraries/LibraryItem.cs8
-rw-r--r--Jellyfin.Data/Entities/Libraries/MediaFile.cs21
-rw-r--r--Jellyfin.Data/Entities/Libraries/MediaFileStream.cs23
-rw-r--r--Jellyfin.Data/Entities/Libraries/MetadataProvider.cs11
-rw-r--r--Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs23
-rw-r--r--Jellyfin.Data/Entities/Libraries/Movie.cs3
-rw-r--r--Jellyfin.Data/Entities/Libraries/MovieMetadata.cs25
-rw-r--r--Jellyfin.Data/Entities/Libraries/MusicAlbum.cs3
-rw-r--r--Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs21
-rw-r--r--Jellyfin.Data/Entities/Libraries/Person.cs13
-rw-r--r--Jellyfin.Data/Entities/Libraries/PersonRole.cs28
-rw-r--r--Jellyfin.Data/Entities/Libraries/Photo.cs3
-rw-r--r--Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs21
-rw-r--r--Jellyfin.Data/Entities/Libraries/Rating.cs23
-rw-r--r--Jellyfin.Data/Entities/Libraries/RatingSource.cs25
-rw-r--r--Jellyfin.Data/Entities/Libraries/Release.cs16
-rw-r--r--Jellyfin.Data/Entities/Libraries/Season.cs22
-rw-r--r--Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs22
-rw-r--r--Jellyfin.Data/Entities/Libraries/Series.cs3
-rw-r--r--Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs30
-rw-r--r--Jellyfin.Data/Entities/Libraries/Track.cs22
-rw-r--r--Jellyfin.Data/Entities/Libraries/TrackMetadata.cs21
-rw-r--r--Jellyfin.Data/Entities/Permission.cs10
-rw-r--r--Jellyfin.Data/Entities/Preference.cs9
-rw-r--r--Jellyfin.Data/Entities/User.cs58
-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.csproj12
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs13
-rw-r--r--Jellyfin.Networking/Jellyfin.Networking.csproj7
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs58
-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/Jellyfin.Server.Implementations.csproj5
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs3
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs9
-rw-r--r--Jellyfin.Server/CoreAppHost.cs5
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs8
-rw-r--r--Jellyfin.Server/Filters/WebsocketModelFilter.cs2
-rw-r--r--Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs2
-rw-r--r--Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs2
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj9
-rw-r--r--Jellyfin.Server/Migrations/MigrationOptions.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs6
-rw-r--r--Jellyfin.Server/Program.cs11
-rw-r--r--Jellyfin.Server/Properties/AssemblyInfo.cs2
-rw-r--r--Jellyfin.Server/StartupOptions.cs4
-rw-r--r--Jellyfin.sln35
-rw-r--r--MediaBrowser.Common/Crc32.cs89
-rw-r--r--MediaBrowser.Common/Extensions/StreamExtensions.cs51
-rw-r--r--MediaBrowser.Common/Extensions/StringExtensions.cs37
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs4
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs2
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs2
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs2
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonStringConverter.cs39
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs3
-rw-r--r--MediaBrowser.Common/Json/JsonDefaults.cs9
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj9
-rw-r--r--MediaBrowser.Common/Net/IPHost.cs99
-rw-r--r--MediaBrowser.Common/Net/IPNetAddress.cs6
-rw-r--r--MediaBrowser.Common/Net/NetworkExtensions.cs5
-rw-r--r--MediaBrowser.Common/Plugins/BasePlugin.cs5
-rw-r--r--MediaBrowser.Common/Plugins/BasePluginOfT.cs36
-rw-r--r--MediaBrowser.Common/Plugins/IPluginManager.cs12
-rw-r--r--MediaBrowser.Common/Plugins/LocalPlugin.cs6
-rw-r--r--MediaBrowser.Common/Progress/ActionableProgress.cs1
-rw-r--r--MediaBrowser.Common/Progress/SimpleProgress.cs1
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs12
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs4
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs2
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs5
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs8
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs19
-rw-r--r--MediaBrowser.Controller/Providers/ILocalImageProvider.cs2
-rw-r--r--MediaBrowser.Controller/Resolvers/BaseItemResolver.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs7
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs14
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj7
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs4
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs6
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs6
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs22
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs101
-rw-r--r--MediaBrowser.MediaEncoding/FfmpegException.cs39
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj8
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs28
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/AssParser.cs133
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs103
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs478
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs63
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs63
-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/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.cs10
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs36
-rw-r--r--MediaBrowser.Model/Dlna/IDeviceDiscovery.cs1
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs4
-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.cs16
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs964
-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.cs7
-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.cs199
-rw-r--r--MediaBrowser.Model/Entities/PackageReviewInfo.cs40
-rw-r--r--MediaBrowser.Model/Entities/ProviderIdsExtensions.cs86
-rw-r--r--MediaBrowser.Model/Entities/SpecialFolder.cs36
-rw-r--r--MediaBrowser.Model/Entities/VirtualFolderInfo.cs20
-rw-r--r--MediaBrowser.Model/Globalization/CultureDto.cs12
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs7
-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.cs38
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj10
-rw-r--r--MediaBrowser.Model/MediaInfo/MediaInfo.cs22
-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.cs5
-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.cs10
-rw-r--r--MediaBrowser.Model/Notifications/NotificationRequest.cs14
-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.cs10
-rw-r--r--MediaBrowser.Model/Querying/LatestItemsQuery.cs9
-rw-r--r--MediaBrowser.Model/Querying/MovieRecommendationQuery.cs14
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs20
-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.cs16
-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/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/SyncJob.cs10
-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.cs4
-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/Users/UserPolicy.cs100
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs9
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs9
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj7
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs2
-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.cs121
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs89
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs14
-rw-r--r--MediaBrowser.Providers/Studios/StudiosImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs27
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj7
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs4
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs29
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs2
-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/SsdpDeviceLocator.cs14
-rw-r--r--deployment/Dockerfile.fedora.amd644
-rw-r--r--jellyfin.ruleset14
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs2
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj16
-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/Extensions/StringExtensionsTests.cs43
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj11
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs52
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs27
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs39
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs36
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj11
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj11
-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/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/example.ssa20
-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/Jellyfin.Model.Tests.csproj11
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj13
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs35
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs45
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs4
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj (renamed from tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj)22
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs (renamed from tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs)103
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj13
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs65
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs30
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs58
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json684
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs57
-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/StartupControllerTests.cs119
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj40
-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)2
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs55
-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.Tests/Jellyfin.Server.Tests.csproj36
-rw-r--r--tests/Jellyfin.Server.Tests/ParseNetworkTests.cs (renamed from tests/Jellyfin.Api.Tests/ParseNetworkTests.cs)18
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj13
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs22
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo20
424 files changed, 6273 insertions, 5143 deletions
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/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 1200275d5..954315f0e 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -49,6 +49,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)
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/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 21ba1c755..c94d803e1 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -36,7 +36,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);
@@ -333,7 +333,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 +400,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 +559,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..480621dd7 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -25,7 +25,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/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 82490ec31..d3e9a41ec 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -128,7 +128,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 +229,10 @@ namespace Emby.Dlna.Main
{
try
{
- ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ if (communicationsServer != null)
+ {
+ ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
+ }
}
catch (Exception ex)
{
@@ -313,9 +317,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/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 938ce5fbf..7bf7047fb 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -235,7 +235,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;
@@ -270,7 +276,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);
}
@@ -291,7 +303,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);
@@ -325,14 +343,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
{
@@ -396,7 +421,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);
@@ -414,7 +445,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;
@@ -990,7 +1027,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/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 315be1e8b..5abc1bc13 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -132,7 +132,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{
- if (_disposed)
+ if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
return;
}
@@ -777,7 +777,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;
}
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index 557bc69a7..b7643fb27 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -45,10 +45,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 +94,10 @@ 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);
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.PreserveWhitespace,
+ cancellationToken).ConfigureAwait(false);
}
private async Task<HttpResponseMessage> PostSoapDataAsync(
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
index 0865968ad..cbcf66e45 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)
{
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/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/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
index 8c7d961f3..d13871add 100644
--- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs
+++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
@@ -69,7 +69,7 @@ namespace Emby.Dlna.Ssdp
{
lock (_syncLock)
{
- if (_listenerCount > 0 && _deviceLocator == null)
+ if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
{
_deviceLocator = new SsdpDeviceLocator(_commsServer);
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..aa8a3d212 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -352,8 +352,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.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..63116f368 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -44,7 +44,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/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/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 09a030d2d..7b6a1705b 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -221,20 +221,21 @@ namespace Emby.Naming.Video
string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
- if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
+ // Remove the folder name before cleaning as we don't care about cleaning that part
+ if (folderName.Length <= testFilename.Length)
{
- testFilename = cleanName.ToString();
+ testFilename = testFilename.Substring(folderName.Length).Trim();
}
- if (folderName.Length <= testFilename.Length)
+ if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
- testFilename = testFilename.Substring(folderName.Length).Trim();
+ testFilename = cleanName.Trim().ToString();
}
+ // The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(testFilename)
- || testFilename[0] == '-'
- || testFilename[0] == '_'
- || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
+ || testFilename[0] == '-'
+ || Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
}
return false;
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 619d1520e..79a6da8f7 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
@@ -146,7 +147,7 @@ namespace Emby.Naming.Video
/// <param name="name">Raw name.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
- public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
+ public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
{
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
}
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 16ee918c4..526a27229 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -25,7 +25,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/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 62e33e6c4..e64a658c5 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -28,7 +28,6 @@
<!-- 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" />
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 77819c764..3f7076383 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -53,7 +53,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 788e08e48..3846de5fd 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -10,8 +10,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 +41,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 +49,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 +97,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 +117,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 +126,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.
@@ -229,6 +228,11 @@ namespace Emby.Server.Implementations
public int HttpsPort { get; private set; }
/// <summary>
+ /// Gets the value of the PublishedServerUrl setting.
+ /// </summary>
+ public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
+
+ /// <summary>
/// Gets the server configuration manager.
/// </summary>
/// <value>The server configuration manager.</value>
@@ -240,12 +244,14 @@ 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)
{
@@ -268,6 +274,7 @@ namespace Emby.Server.Implementations
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_startupOptions = options;
+ _startupConfig = startupConfig;
// Initialize runtime stat collection
if (ServerConfigurationManager.Configuration.EnableMetrics)
@@ -374,7 +381,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);
@@ -460,7 +467,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 +491,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 +507,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();
@@ -1147,10 +1162,10 @@ namespace Emby.Server.Implementations
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);
@@ -1167,10 +1182,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);
@@ -1187,10 +1202,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);
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 8c5fa09f6..87ebe960a 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -49,7 +49,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/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 3011a37e3..db532ce5b 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -107,7 +107,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();
}
@@ -344,7 +344,20 @@ namespace Emby.Server.Implementations.Collections
}
else
{
- results[item.Id] = item;
+ var alreadyInResults = false;
+ foreach (var child in item.GetMediaSources(true))
+ {
+ if (Guid.TryParse(child.Id, out var id) && results.ContainsKey(id))
+ {
+ alreadyInResults = true;
+ break;
+ }
+ }
+
+ if (!alreadyInResults)
+ {
+ results[item.Id] = item;
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 1af301ceb..a04d63088 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using SQLitePCL.pretty;
@@ -59,7 +60,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(conn =>
{
- conn.ExecuteAll(string.Join(";", queries));
+ conn.ExecuteAll(string.Join(';', queries));
});
}
@@ -142,11 +143,10 @@ namespace Emby.Server.Implementations.Data
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)
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 6e1f2feae..2ae805447 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -88,7 +88,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");
}
@@ -687,7 +687,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 +749,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 +758,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 +785,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 +807,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
{
@@ -902,7 +902,7 @@ namespace Emby.Server.Implementations.Data
if (item.ProductionLocations.Length > 0)
{
- saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations));
+ saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
}
else
{
@@ -911,7 +911,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 +931,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 +940,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);
@@ -2549,7 +2549,7 @@ namespace Emby.Server.Implementations.Data
if (groups.Count > 0)
{
- return " Group by " + string.Join(",", groups);
+ return " Group by " + string.Join(',', groups);
}
return string.Empty;
@@ -2578,7 +2578,7 @@ namespace Emby.Server.Implementations.Data
}
var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
+ + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
+ GetFromText()
+ GetJoinUserDataText(query);
@@ -2630,7 +2630,7 @@ namespace Emby.Server.Implementations.Data
}
var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
+ + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
+ GetFromText()
+ GetJoinUserDataText(query);
@@ -2880,7 +2880,7 @@ namespace Emby.Server.Implementations.Data
}
var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
+ + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
+ GetFromText()
+ GetJoinUserDataText(query);
@@ -2923,15 +2923,15 @@ namespace Emby.Server.Implementations.Data
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
}
else
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
}
commandText += GetJoinUserDataText(query)
@@ -3039,7 +3039,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);
@@ -3137,7 +3137,7 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
+ + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
+ GetFromText()
+ GetJoinUserDataText(query);
@@ -3203,7 +3203,7 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
+ var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
@@ -3284,7 +3284,7 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
var commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
+ + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
+ GetFromText()
+ GetJoinUserDataText(query);
@@ -3327,15 +3327,15 @@ namespace Emby.Server.Implementations.Data
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
}
else
{
- commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
}
commandText += GetJoinUserDataText(query)
@@ -3596,7 +3596,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 +3607,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 +3618,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 +4351,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 +4401,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 + ")");
}
@@ -4498,7 +4498,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)
@@ -4549,7 +4549,7 @@ namespace Emby.Server.Implementations.Data
}
else if (enableItemsByName && includedItemByNameTypes.Count > 1)
{
- var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
}
else
@@ -4564,7 +4564,7 @@ namespace Emby.Server.Implementations.Data
}
else if (queryTopParentIds.Length > 1)
{
- var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+ var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
@@ -4576,7 +4576,7 @@ namespace Emby.Server.Implementations.Data
}
else if (enableItemsByName && includedItemByNameTypes.Count > 1)
{
- var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
}
else
@@ -4597,7 +4597,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));
}
@@ -5148,7 +5148,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 +5162,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 + ")");
}
@@ -5308,19 +5308,19 @@ AND Type = @InternalPersonType)");
var typeClause = itemValueTypes.Length == 1 ?
("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+ ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
var commandText = "Select Value From ItemValues where " + typeClause;
if (withItemTypes.Count > 0)
{
- var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'"));
+ var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'"));
commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
}
if (excludeItemTypes.Count > 0)
{
- var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'"));
+ var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'"));
commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
}
@@ -5363,7 +5363,7 @@ AND Type = @InternalPersonType)");
var typeClause = itemValueTypes.Length == 1 ?
("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+ ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
InternalItemsQuery typeSubQuery = null;
@@ -5427,7 +5427,7 @@ AND Type = @InternalPersonType)");
columns = GetFinalColumnsToSelect(query, columns);
var commandText = "select "
- + string.Join(",", columns)
+ + string.Join(',', columns)
+ GetFromText()
+ GetJoinUserDataText(query);
@@ -5504,7 +5504,7 @@ AND Type = @InternalPersonType)");
if (query.EnableTotalRecordCount)
{
var countText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+ + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+ GetFromText()
+ GetJoinUserDataText(query)
+ whereText;
@@ -5565,7 +5565,7 @@ AND Type = @InternalPersonType)");
if (query.EnableTotalRecordCount)
{
commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+ + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+ GetFromText()
+ GetJoinUserDataText(query)
+ whereText;
@@ -6207,9 +6207,9 @@ AND Type = @InternalPersonType)");
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;
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 2c4e8e0fc..6574db607 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -47,7 +47,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)",
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 522667153..be552ef93 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -27,9 +27,10 @@
<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.3" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
- <PackageReference Include="sharpcompress" Version="0.28.0" />
+ <PackageReference Include="sharpcompress" Version="0.28.1" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
@@ -49,7 +50,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/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 9486874d5..3624e079f 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,3 +1,6 @@
+#nullable enable
+
+using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
@@ -29,7 +32,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,6 +52,8 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc />
public Task RunAsync()
{
+ CheckDisposed();
+
try
{
_udpServer = new UdpServer(_logger, _appHost, _config);
@@ -62,6 +67,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 +84,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/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7e0c2c1da..06acb5606 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -56,7 +56,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/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index 4bef59543..0b823ff06 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,5 +1,5 @@
#pragma warning disable CS1591
-
+#nullable enable
using System;
namespace Emby.Server.Implementations
@@ -9,7 +9,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 +19,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/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index db27862ce..46a7feb7f 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1240,11 +1240,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>
@@ -2767,6 +2776,7 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
{
+ string newPath;
if (ownerItem != null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2774,15 +2784,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 +2795,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 +2813,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))
+ if (path.TryReplaceSubPath(from, to, out var newPath))
{
- throw new ArgumentNullException(nameof(path));
+ return newPath;
}
- if (string.IsNullOrWhiteSpace(from))
- {
- throw new ArgumentNullException(nameof(from));
- }
-
- if (string.IsNullOrWhiteSpace(to))
- {
- throw new ArgumentNullException(nameof(to));
- }
-
- 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)
@@ -2956,7 +2917,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 +2951,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>());
}
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 2070df31e..c2951dd15 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -25,7 +25,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..b2943020c 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -46,7 +46,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;
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 06ff3e611..57d0c26b9 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -1,6 +1,8 @@
#nullable enable
using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Text.RegularExpressions;
namespace Emby.Server.Implementations.Library
@@ -47,5 +49,66 @@ namespace Emby.Server.Implementations.Library
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)
+ || (!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/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 2f5e46038..6e688693b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -30,7 +30,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,7 +42,7 @@ 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();
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 86242d137..0525c7e30 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -13,7 +13,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/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 8ef7172de..714bc3a84 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -69,6 +69,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,
@@ -217,110 +321,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>
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 2f7af60c0..9b4cd7a3d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -12,11 +13,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;
@@ -34,11 +44,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
season = parent.GetParents().OfType<Season>().FirstOrDefault();
}
- // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
+ // 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);
@@ -74,14 +85,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/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 341194f23..7a6b1d8b6 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -45,7 +45,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 +71,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..91a21db60 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1856,7 +1856,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 +1921,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
{
@@ -2604,7 +2606,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Movies",
- CollectionType = CollectionType.Movies
+ CollectionType = CollectionTypeOptions.Movies
};
}
@@ -2615,7 +2617,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..44a8cdee4 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -28,7 +28,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;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index 57424f043..c20b08088 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -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/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 7567ea312..1926e738f 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -35,8 +35,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 +111,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 +122,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 +182,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 +703,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 +775,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");
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 5ef83f274..68173a0ef 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_networkManager = networkManager;
_streamHelper = streamHelper;
- _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions = JsonDefaults.Options;
}
public string Name => "HD Homerun";
@@ -335,11 +335,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];
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index f09338330..20a4d87fb 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -2,6 +2,7 @@
using System;
using System.Buffers;
+using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
@@ -10,6 +11,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,13 +122,13 @@ 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);
+
+ int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
ParseReturnMessage(buffer, receivedBytes, out string returnVal);
@@ -166,9 +168,9 @@ 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 _))
@@ -178,9 +180,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
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 _))
@@ -191,10 +193,10 @@ 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 _))
@@ -232,9 +234,9 @@ 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 _))
@@ -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,107 +285,74 @@ 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);
+ private 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 len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
- int messageLength = byteName.Length + byteValue.Length + 12;
- if (lockkey.HasValue)
- {
- messageLength += 6;
- }
+ // TODO: variable length: this can be 2 bytes if len > 127
+ // Write length in front of value
+ buffer[0] = Convert.ToByte(len);
- var message = new byte[messageLength];
+ // null-terminate
+ buffer[len++] = 0;
- int offset = InsertHeaderAndName(byteName, messageLength, message);
+ return len;
+ }
- bool flipEndian = BitConverter.IsLittleEndian;
+ private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
+ {
+ // Packet type
+ BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
- 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);
- }
+ // We write the payload length at the end
+ int offset = 4;
- Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4);
- offset += 4;
- }
+ // Tag
+ buffer[offset++] = GetSetName;
- // calculate crc and insert at the end of the message
- var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
- if (flipEndian)
- {
- Array.Reverse(crcBytes);
- }
-
- 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;
-
- // 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);
+ // Payload length
+ BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
- // insert name string
- Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
- offset += 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);
- return offset;
+ return offset + 4;
}
private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
@@ -442,90 +411,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
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)
- {
- var hash = 0xffffffff;
- for (var i = 0; i < numBytes; i++)
- {
- hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
- }
-
- 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;
- }
- }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index cf653f87d..b16ccc561 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -92,7 +92,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();
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index c82b67b41..c4f173c7a 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -155,7 +155,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..eeb2426f4 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -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..b029b7042 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -112,5 +112,8 @@
"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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 4ee4eb989..051d6d009 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",
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/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index faee2519a..11139d32a 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -7,5 +7,77 @@
"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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index e5707e78c..ef8070503 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -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",
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/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index a321e35d0..829a29ad4 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -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ı.",
+ "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.",
"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.",
+ "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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index d4cb592ef..9920ef4d5 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -113,5 +113,9 @@
"TasksChannelsCategory": "Internetiniai Kanalai",
"TasksApplicationCategory": "Programa",
"TasksLibraryCategory": "Mediateka",
- "TasksMaintenanceCategory": "Priežiūra"
+ "TasksMaintenanceCategory": "Priežiūra",
+ "TaskCleanActivityLog": "Švarus veiklos žurnalas",
+ "Undefined": "Neapibrėžtas",
+ "Forced": "Priverstas",
+ "Default": "Numatytas"
}
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/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..b5a7fa5b8 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -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/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 71dd2c7a3..5bf58baf8 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,6 @@
"System": "ระบบ",
"Sync": "ซิงค์",
"SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
- "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
+ "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่",
+ "Default": "ค่าเริ่มต้น"
}
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/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 3f9e22106..98de848bc 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -36,7 +36,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.
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index f27305cbe..c6e931448 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -166,7 +166,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/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index c26ccfd88..700396c4c 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,8 +1,10 @@
#nullable enable
+
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
@@ -11,9 +13,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 +34,24 @@ 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
+ {
+ if (_httpClientFactory == null)
+ {
+ _httpClientFactory = _appHost.Resolve<IHttpClientFactory>();
+ }
+
+ return _httpClientFactory;
+ }
+ }
+
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
@@ -51,7 +70,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 +94,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.
@@ -332,32 +351,74 @@ 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 due to invalid value. {Path}", path);
+ return false;
+ }
+ }
+
+ /// <inheritdoc/>
+ public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path)
+ {
+ if (packageInfo == null)
{
- _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
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 = new Guid(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),
+ Version = versionInfo.Version,
+ Status = PluginStatus.Active,
+ AutoUpdate = true,
+ ImagePath = imagePath
+ };
+
+ return SaveManifest(manifest, path);
}
/// <summary>
@@ -410,7 +471,7 @@ namespace Emby.Server.Implementations.Plugins
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 +678,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/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index b302303f8..a145a8423 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -69,7 +69,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.
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 171e44258..649305fd5 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -143,7 +143,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
Directory.CreateDirectory(parentPath);
- string text = string.Join("|", previouslyFailedImages);
+ string text = string.Join('|', previouslyFailedImages);
File.WriteAllText(failHistoryPath, text);
}
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index f9c6a13c6..a653b58c2 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -8,7 +8,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/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 4fd7ac0c1..d01184e0b 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -79,7 +79,7 @@ namespace Emby.Server.Implementations.Udp
/// Starts the specified port.
/// </summary>
/// <param name="port">The port.</param>
- /// <param name="cancellationToken"></param>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
public void Start(int port, CancellationToken cancellationToken)
{
_endpoint = new IPEndPoint(IPAddress.Any, port);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index abcb4313f..fc34f93cd 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.Updates
_httpClientFactory = httpClientFactory;
_config = config;
_zipClient = zipClient;
- _jsonSerializerOptions = JsonDefaults.GetOptions();
+ _jsonSerializerOptions = JsonDefaults.Options;
_pluginManager = pluginManager;
}
@@ -192,17 +192,12 @@ namespace Emby.Server.Implementations.Updates
var version = package.Versions[i];
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
- // Update the manifests, if anything changes.
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);
}
- // 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);
@@ -294,7 +289,8 @@ namespace Emby.Server.Implementations.Updates
Name = package.Name,
Version = v.VersionNumber,
SourceUrl = v.SourceUrl,
- Checksum = v.Checksum
+ Checksum = v.Checksum,
+ PackageInfo = package
};
}
}
@@ -571,24 +567,16 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
+ await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir);
_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
_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/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 616fe5b91..8b1813b20 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,
@@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -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,
@@ -339,7 +339,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index e1c9f69f6..049a4bed7 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -25,7 +25,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.
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..f6c23c5aa 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -203,7 +203,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,7 +218,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] bool enableAdaptiveBitrateStreaming = true)
{
@@ -255,7 +255,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -270,7 +270,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -370,7 +370,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,7 +385,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] bool enableAdaptiveBitrateStreaming = true)
{
@@ -422,7 +422,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -437,7 +437,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -533,7 +533,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,7 +548,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)
{
var cancellationTokenSource = new CancellationTokenSource();
@@ -585,7 +585,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -600,7 +600,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -698,7 +698,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,7 +713,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)
{
var cancellationTokenSource = new CancellationTokenSource();
@@ -750,7 +750,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -765,7 +765,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -868,7 +868,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,7 +883,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)
{
var streamingRequest = new VideoRequestDto
@@ -920,7 +920,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -935,7 +935,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -1040,7 +1040,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,7 +1055,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)
{
var streamingRequest = new StreamingRequestDto
@@ -1092,7 +1092,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -1107,7 +1107,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
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..89037749a 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)
@@ -741,7 +751,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,
@@ -820,7 +830,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,
@@ -900,7 +910,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,
@@ -978,7 +988,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,
@@ -1058,7 +1068,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,
@@ -1136,7 +1146,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,
@@ -1216,7 +1226,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,
@@ -1469,7 +1479,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageIndex)
{
var user = _userManager.GetUserById(userId);
- if (user == null)
+ if (user?.ProfileImage == null)
{
return NotFound();
}
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..dabd4deb7 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -344,11 +344,12 @@ namespace Jellyfin.Api.Controllers
Directory.CreateDirectory(directory);
using (var stream = result.Content)
{
+ // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var fileStream = new FileStream(
fullCachePath,
FileMode.Create,
FileAccess.Write,
- FileShare.Read,
+ FileShare.None,
IODefaults.FileStreamBufferSize,
true);
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/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index db4aa9668..f8e8825ef 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -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,15 +590,15 @@ 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);
}
@@ -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/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/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..24285bfb9 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -45,7 +45,7 @@ namespace Jellyfin.Api.Controllers
{
_installationManager = installationManager;
_pluginManager = pluginManager;
- _serializerOptions = JsonDefaults.GetOptions();
+ _serializerOptions = JsonDefaults.Options;
_config = config;
}
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 5284888d8..e226adc64 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -259,7 +259,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/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/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index bacd95bac..0c2e6f19f 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);
@@ -223,7 +223,7 @@ namespace Jellyfin.Api.Controllers
DeInterlace = true,
RequireNonAnamorphic = true,
EnableMpegtsM2TsMode = true,
- 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,
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
};
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index ba51aa43e..620eef568 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -198,7 +198,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 +213,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,
@@ -253,7 +253,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -268,7 +268,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 44dc63952..99654e7b0 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -217,9 +217,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 +362,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,7 +377,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)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
@@ -418,7 +416,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -433,7 +431,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
- Context = context,
+ Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -620,7 +618,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 +633,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..e0c744325 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -113,14 +113,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/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 92ff42b49..16380f0bb 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
+using System.Net.Mime;
using System.Security.Claims;
using System.Text;
using System.Threading;
@@ -171,13 +172,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,7 +225,7 @@ 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);
@@ -560,13 +563,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/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 0d8315dee..ce6740fc9 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -523,7 +523,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)
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 4957ee8b8..e2306aa27 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -183,7 +183,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 +245,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);
@@ -508,17 +508,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/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 5f7c64a7e..d5372d752 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -17,8 +17,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="6.0.5" />
- <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.0.5" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="6.0.7" />
+ <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.0.7" />
</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/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
index 872a46824..e33e552ed 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)
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..7974d3add 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,22 +26,12 @@ 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.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[XmlIgnore]
- [Key]
- [Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
@@ -51,41 +39,24 @@ namespace Jellyfin.Data.Entities
/// Gets or sets the id of the associated user.
/// </summary>
[XmlIgnore]
- [Required]
public Guid UserId { get; protected 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..e4534e8b5 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,15 +34,7 @@ namespace Jellyfin.Data.Entities
Type = type;
UserId = userId;
DateCreated = DateTime.UtcNow;
- LogSeverity = logLevel;
- }
-
- /// <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()
- {
+ LogSeverity = LogLevel.Information;
}
/// <summary>
@@ -59,7 +50,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 512.
/// </remarks>
- [Required]
[MaxLength(512)]
[StringLength(512)]
public string Name { get; set; }
@@ -72,7 +62,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 +72,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 +80,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 256.
/// </remarks>
- [Required]
[MaxLength(256)]
[StringLength(256)]
public string Type { get; set; }
@@ -111,7 +100,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.
diff --git a/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs b/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs
index 511e3b281..cc46248c7 100644
--- a/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs
@@ -15,22 +15,15 @@ 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;
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CustomItemDisplayPreferences"/> class.
- /// </summary>
- protected CustomItemDisplayPreferences()
- {
+ Key = key;
+ Value = value;
}
/// <summary>
@@ -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; }
}
}
diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs
index 1a8ca1da3..64cd6812a 100644
--- a/Jellyfin.Data/Entities/DisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/DisplayPreferences.cs
@@ -30,20 +30,11 @@ 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.
/// </summary>
/// <remarks>
@@ -74,7 +65,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required. Max Length = 32.
/// </remarks>
- [Required]
[MaxLength(32)]
[StringLength(32)]
public string Client { get; set; }
@@ -145,14 +135,14 @@ 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.
diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs
index 878811e59..b14e22b7b 100644
--- a/Jellyfin.Data/Entities/Group.cs
+++ b/Jellyfin.Data/Entities/Group.cs
@@ -33,16 +33,6 @@ 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.
/// </summary>
/// <remarks>
@@ -56,7 +46,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string Name { get; set; }
diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs
index 062046260..5adc52491 100644
--- a/Jellyfin.Data/Entities/HomeSection.cs
+++ b/Jellyfin.Data/Entities/HomeSection.cs
@@ -15,7 +15,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Identity. Required.
/// </remarks>
- [Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
diff --git a/Jellyfin.Data/Entities/ImageInfo.cs b/Jellyfin.Data/Entities/ImageInfo.cs
index ab8452e62..e0c37047d 100644
--- a/Jellyfin.Data/Entities/ImageInfo.cs
+++ b/Jellyfin.Data/Entities/ImageInfo.cs
@@ -20,16 +20,6 @@ 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.
/// </summary>
/// <remarks>
@@ -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..4bfeb2fa3 100644
--- a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
+++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs
@@ -29,13 +29,6 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
- /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
- /// </summary>
- protected ItemDisplayPreferences()
- {
- }
-
- /// <summary>
/// Gets or sets the Id.
/// </summary>
/// <remarks>
@@ -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..84a524de2 100644
--- a/Jellyfin.Data/Entities/Libraries/Artwork.cs
+++ b/Jellyfin.Data/Entities/Libraries/Artwork.cs
@@ -18,8 +18,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,18 +27,6 @@ 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>
@@ -57,7 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 65535.
/// </remarks>
- [Required]
[MaxLength(65535)]
[StringLength(65535)]
public string Path { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/Book.cs b/Jellyfin.Data/Entities/Libraries/Book.cs
index 2e63f75bd..aea3d58d5 100644
--- a/Jellyfin.Data/Entities/Libraries/Book.cs
+++ b/Jellyfin.Data/Entities/Libraries/Book.cs
@@ -13,7 +13,8 @@ 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>();
diff --git a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs
index 4a3d290f0..1ff4327b0 100644
--- a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs
@@ -1,8 +1,6 @@
#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,30 +15,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="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; }
@@ -51,7 +31,6 @@ namespace Jellyfin.Data.Entities.Libraries
public virtual ICollection<Company> Publishers { get; protected 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..11f53ae20 100644
--- a/Jellyfin.Data/Entities/Libraries/Chapter.cs
+++ b/Jellyfin.Data/Entities/Libraries/Chapter.cs
@@ -17,8 +17,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,23 +26,6 @@ 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>
@@ -63,7 +45,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 +54,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)]
diff --git a/Jellyfin.Data/Entities/Libraries/Collection.cs b/Jellyfin.Data/Entities/Libraries/Collection.cs
index 39eded752..4253b7ecc 100644
--- a/Jellyfin.Data/Entities/Libraries/Collection.cs
+++ b/Jellyfin.Data/Entities/Libraries/Collection.cs
@@ -1,3 +1,4 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
#pragma warning disable CA2227
using System.Collections.Generic;
@@ -37,7 +38,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// </remarks>
[MaxLength(1024)]
[StringLength(1024)]
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
index f9539964d..e19362bdf 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>
@@ -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..09050bb52 100644
--- a/Jellyfin.Data/Entities/Libraries/Company.cs
+++ b/Jellyfin.Data/Entities/Libraries/Company.cs
@@ -15,22 +15,10 @@ 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>();
- }
-
- /// <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()
- {
+ ChildCompanies = new HashSet<Company>();
}
/// <summary>
@@ -57,7 +45,6 @@ namespace Jellyfin.Data.Entities.Libraries
public virtual ICollection<Company> ChildCompanies { get; protected 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..88d1a0c25 100644
--- a/Jellyfin.Data/Entities/Libraries/CustomItem.cs
+++ b/Jellyfin.Data/Entities/Libraries/CustomItem.cs
@@ -13,7 +13,8 @@ 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>();
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..458c7d9f5 100644
--- a/Jellyfin.Data/Entities/Libraries/Episode.cs
+++ b/Jellyfin.Data/Entities/Libraries/Episode.cs
@@ -1,6 +1,5 @@
#pragma warning disable CA2227
-using System;
using System.Collections.Generic;
using Jellyfin.Data.Interfaces;
@@ -14,31 +13,14 @@ 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; }
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..9f3d65028 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,32 +13,9 @@ 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>
@@ -57,7 +33,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Indexed, Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string Name { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
index d74330c05..d12e011a8 100644
--- a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
@@ -43,16 +43,6 @@ 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.
/// </summary>
/// <remarks>
@@ -67,7 +57,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 1024.
/// </remarks>
- [Required]
[MaxLength(1024)]
[StringLength(1024)]
public string Title { get; set; }
@@ -80,7 +69,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 +79,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 +88,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)]
diff --git a/Jellyfin.Data/Entities/Libraries/Library.cs b/Jellyfin.Data/Entities/Libraries/Library.cs
index 4f82a2e2a..e45384902 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,24 +13,11 @@ 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;
- }
-
- /// <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()
- {
+ Path = path;
}
/// <summary>
@@ -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,7 +45,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required.
/// </remarks>
- [Required]
public string Path { get; set; }
/// <inheritdoc />
diff --git a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs
index a9167aa7f..67ffad944 100644
--- a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs
+++ b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs
@@ -21,13 +21,6 @@ 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.
/// </summary>
/// <remarks>
@@ -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..f3e2fe653 100644
--- a/Jellyfin.Data/Entities/Libraries/MediaFile.cs
+++ b/Jellyfin.Data/Entities/Libraries/MediaFile.cs
@@ -19,8 +19,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,27 +29,10 @@ 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.
/// </summary>
/// <remarks>
@@ -65,7 +47,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 65535.
/// </remarks>
- [Required]
[MaxLength(65535)]
[StringLength(65535)]
public string Path { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs
index 5b03e260e..ba21294fc 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,27 +15,9 @@ 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>
diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs
index a18a612bc..fb2587882 100644
--- a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs
+++ b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs
@@ -25,16 +25,6 @@ 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.
/// </summary>
/// <remarks>
@@ -49,7 +39,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 1024.
/// </remarks>
- [Required]
[MaxLength(1024)]
[StringLength(1024)]
public string Name { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs
index fcfb35bfa..2a9c904c8 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,23 +23,7 @@ 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>
@@ -57,7 +41,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string ProviderId { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/Movie.cs b/Jellyfin.Data/Entities/Libraries/Movie.cs
index 08db904fa..f89cacff4 100644
--- a/Jellyfin.Data/Entities/Libraries/Movie.cs
+++ b/Jellyfin.Data/Entities/Libraries/Movie.cs
@@ -13,7 +13,8 @@ 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>();
diff --git a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs
index aa1501a5c..fb181dea6 100644
--- a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Interfaces;
namespace Jellyfin.Data.Entities.Libraries
@@ -17,22 +16,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 +29,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 +39,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 +49,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,7 +59,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 studios that produced this movie.
@@ -81,7 +67,6 @@ namespace Jellyfin.Data.Entities.Libraries
public virtual ICollection<Company> Studios { get; protected 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..4049cdac8 100644
--- a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs
+++ b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs
@@ -12,7 +12,8 @@ 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>();
diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs
index 05c0b0374..3080bd692 100644
--- a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs
@@ -15,22 +15,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 +28,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 +38,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,7 +48,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 a collection containing the labels.
diff --git a/Jellyfin.Data/Entities/Libraries/Person.cs b/Jellyfin.Data/Entities/Libraries/Person.cs
index af4c87b73..159bd47be 100644
--- a/Jellyfin.Data/Entities/Libraries/Person.cs
+++ b/Jellyfin.Data/Entities/Libraries/Person.cs
@@ -32,16 +32,6 @@ 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.
/// </summary>
/// <remarks>
@@ -56,7 +46,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 1024.
/// </remarks>
- [Required]
[MaxLength(1024)]
[StringLength(1024)]
public string Name { get; set; }
@@ -69,7 +58,7 @@ 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.
diff --git a/Jellyfin.Data/Entities/Libraries/PersonRole.cs b/Jellyfin.Data/Entities/Libraries/PersonRole.cs
index cd38ee83d..988aa84ba 100644
--- a/Jellyfin.Data/Entities/Libraries/PersonRole.cs
+++ b/Jellyfin.Data/Entities/Libraries/PersonRole.cs
@@ -1,6 +1,5 @@
#pragma warning disable CA2227
-using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -18,32 +17,16 @@ 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.
/// </summary>
/// <remarks>
@@ -60,7 +43,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.
@@ -80,7 +63,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required.
/// </remarks>
- [Required]
public virtual Person Person { get; set; }
/// <inheritdoc />
diff --git a/Jellyfin.Data/Entities/Libraries/Photo.cs b/Jellyfin.Data/Entities/Libraries/Photo.cs
index 25562ec96..eb5c96267 100644
--- a/Jellyfin.Data/Entities/Libraries/Photo.cs
+++ b/Jellyfin.Data/Entities/Libraries/Photo.cs
@@ -13,7 +13,8 @@ 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>();
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..6862012a8 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,27 +13,9 @@ 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>
@@ -67,7 +48,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// 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..ae0d806ff 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,28 +14,10 @@ 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>
@@ -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.
@@ -81,7 +62,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// <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..21d403979 100644
--- a/Jellyfin.Data/Entities/Libraries/Release.cs
+++ b/Jellyfin.Data/Entities/Libraries/Release.cs
@@ -17,8 +17,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,23 +26,11 @@ 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.
/// </summary>
/// <remarks>
@@ -58,7 +45,6 @@ namespace Jellyfin.Data.Entities.Libraries
/// <remarks>
/// Required, Max length = 1024.
/// </remarks>
- [Required]
[MaxLength(1024)]
[StringLength(1024)]
public string Name { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/Season.cs b/Jellyfin.Data/Entities/Libraries/Season.cs
index eb6674dbc..04f723a1d 100644
--- a/Jellyfin.Data/Entities/Libraries/Season.cs
+++ b/Jellyfin.Data/Entities/Libraries/Season.cs
@@ -1,6 +1,5 @@
#pragma warning disable CA2227
-using System;
using System.Collections.Generic;
namespace Jellyfin.Data.Entities.Libraries
@@ -13,31 +12,14 @@ 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; }
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..59508831e 100644
--- a/Jellyfin.Data/Entities/Libraries/Series.cs
+++ b/Jellyfin.Data/Entities/Libraries/Series.cs
@@ -13,7 +13,8 @@ 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>();
diff --git a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs
index 877dbfc69..730deccae 100644
--- a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs
+++ b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs
@@ -1,6 +1,5 @@
#pragma warning disable CA2227
-using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -18,30 +17,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 +30,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 +40,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 +50,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,7 +60,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 a collection containing the networks.
@@ -87,7 +68,6 @@ namespace Jellyfin.Data.Entities.Libraries
public virtual ICollection<Company> Networks { get; protected 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..86a3edff8 100644
--- a/Jellyfin.Data/Entities/Libraries/Track.cs
+++ b/Jellyfin.Data/Entities/Libraries/Track.cs
@@ -1,6 +1,5 @@
#pragma warning disable CA2227
-using System;
using System.Collections.Generic;
using Jellyfin.Data.Interfaces;
@@ -14,31 +13,14 @@ 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; }
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..f059dedfa 100644
--- a/Jellyfin.Data/Entities/Permission.cs
+++ b/Jellyfin.Data/Entities/Permission.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Enums;
@@ -23,14 +25,6 @@ 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.
/// </summary>
/// <remarks>
diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs
index 4efddf2a4..a8813ab88 100644
--- a/Jellyfin.Data/Entities/Preference.cs
+++ b/Jellyfin.Data/Entities/Preference.cs
@@ -24,14 +24,6 @@ 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.
/// </summary>
/// <remarks>
@@ -54,7 +46,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 65535.
/// </remarks>
- [Required]
[MaxLength(65535)]
[StringLength(65535)]
public string Value { get; set; }
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 362f3b4eb..74331726c 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -51,6 +51,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 +73,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 +90,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string Username { get; set; }
@@ -113,7 +102,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 +112,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 +130,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 +138,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string AuthenticationProviderId { get; set; }
@@ -160,7 +148,6 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
- [Required]
[MaxLength(255)]
[StringLength(255)]
public string PasswordResetProviderId { get; set; }
@@ -217,7 +204,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,7 +299,7 @@ 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.
@@ -320,8 +307,7 @@ namespace Jellyfin.Data.Entities
/// <remarks>
/// Required.
/// </remarks>
- [Required]
- public virtual DisplayPreferences DisplayPreferences { get; set; }
+ public virtual ICollection<DisplayPreferences> DisplayPreferences { get; set; }
/// <summary>
/// Gets or sets the level of sync play permissions this user has.
@@ -494,18 +480,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 +509,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/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..3fc44640b 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>
@@ -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..fd7cb5ec5 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -274,8 +274,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;
@@ -345,11 +345,6 @@ namespace Jellyfin.Drawing.Skia
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 +442,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,7 +454,7 @@ 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);
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..d2e9dcf9e 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;
@@ -284,14 +285,25 @@ 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;
@@ -413,7 +425,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;
}
@@ -691,11 +703,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 +724,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 +742,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,11 +928,19 @@ 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.
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/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 4f24da0ee..96a4fa2fb 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,6 +27,8 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
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..a3e9516b9 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);
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 76d1389ca..50d7612f2 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -184,12 +184,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;
@@ -444,7 +447,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);
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/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 77f6695bb..d26ac251c 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() ?? "0.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)
}
}
});
diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/WebsocketModelFilter.cs
index 248802857..38afb201d 100644
--- a/Jellyfin.Server/Filters/WebsocketModelFilter.cs
+++ b/Jellyfin.Server/Filters/WebsocketModelFilter.cs
@@ -25,6 +25,8 @@ 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);
}
}
}
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/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..09799307b 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -13,7 +13,9 @@
<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,16 +28,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>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
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/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
index 0925a87b5..bf0225e98 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -32,7 +32,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/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 33f039c39..a15a38177 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;
@@ -104,7 +102,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..4f203b7a9 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -164,6 +164,7 @@ namespace Jellyfin.Server
appPaths,
_loggerFactory,
options,
+ startupConfig,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
@@ -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,7 @@ namespace Jellyfin.Server
}
finally
{
- appHost?.Dispose();
+ appHost.Dispose();
}
if (_restartOnShutdown)
@@ -280,7 +281,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 +623,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/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index b63434092..6d8210527 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -77,7 +77,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 +94,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..0f36e283c 100644
--- a/Jellyfin.sln
+++ b/Jellyfin.sln
@@ -68,12 +68,18 @@ 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}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Model.Tests", "tests\Jellyfin.Model.Tests\Jellyfin.Model.Tests.csproj", "{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -188,10 +194,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 +202,22 @@ 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
+ {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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -211,9 +229,12 @@ 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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
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/Extensions/StreamExtensions.cs b/MediaBrowser.Common/Extensions/StreamExtensions.cs
new file mode 100644
index 000000000..cd77be7b2
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/StreamExtensions.cs
@@ -0,0 +1,51 @@
+#nullable enable
+
+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="StreamReader" />.
+ /// </summary>
+ /// <param name="reader">The <see cref="StreamReader" /> to read from.</param>
+ /// <returns>All lines in the stream.</returns>
+ public static IEnumerable<string> ReadAllLines(this StreamReader reader)
+ {
+ string? line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ yield return line;
+ }
+ }
+ }
+}
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..c3e4ed6db 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.
@@ -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..d9f6519e9 100644
--- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
@@ -69,7 +69,7 @@ namespace MediaBrowser.Common.Json.Converters
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{
- JsonSerializer.Serialize(writer, value, options);
+ throw new NotImplementedException();
}
}
}
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/JsonPipeDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
index 377db1a44..c408a3be1 100644
--- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
@@ -69,7 +69,7 @@ namespace MediaBrowser.Common.Json.Converters
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{
- JsonSerializer.Serialize(writer, value, options);
+ throw new NotImplementedException();
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs
new file mode 100644
index 000000000..669b3cd07
--- /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);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs
index 37e6f64e3..f69e868cc 100644
--- a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs
+++ b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs
@@ -7,6 +7,9 @@ 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 />
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..0d9f78704 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,8 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
@@ -46,16 +48,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/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs
index 4cede9ab1..d67b6b8e1 100644
--- a/MediaBrowser.Common/Net/IPHost.cs
+++ b/MediaBrowser.Common/Net/IPHost.cs
@@ -128,62 +128,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.OrdinalIgnoreCase);
+ 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 +345,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];
@@ -401,7 +406,7 @@ namespace MediaBrowser.Common.Net
}
// 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();
diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs
index 5fab52eac..59e37a5c6 100644
--- a/MediaBrowser.Common/Net/IPNetAddress.cs
+++ b/MediaBrowser.Common/Net/IPNetAddress.cs
@@ -216,11 +216,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;
diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs
index d07bba249..9c1a0cf49 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
{
diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs
index e228ae7ec..ad5a7338d 100644
--- a/MediaBrowser.Common/Plugins/BasePlugin.cs
+++ b/MediaBrowser.Common/Plugins/BasePlugin.cs
@@ -1,10 +1,7 @@
using System;
using System.IO;
using System.Reflection;
-using System.Runtime.InteropServices;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Common.Plugins
{
@@ -53,7 +50,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..99c226f50 100644
--- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs
+++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
@@ -39,29 +39,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;
- assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
+ 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 idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
- if (idAttributes.Length > 0)
- {
- var attribute = (GuidAttribute)idAttributes[0];
- var assemblyId = new Guid(attribute.Value);
+ SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
- assemblyPlugin.SetId(assemblyId);
- }
+ var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
+ if (idAttributes.Length > 0)
+ {
+ var attribute = (GuidAttribute)idAttributes[0];
+ var assemblyId = new Guid(attribute.Value);
+
+ SetId(assemblyId);
}
}
diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs
index 3da34d8bb..f9a8fb6f7 100644
--- a/MediaBrowser.Common/Plugins/IPluginManager.cs
+++ b/MediaBrowser.Common/Plugins/IPluginManager.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
+using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
@@ -16,7 +17,7 @@ namespace MediaBrowser.Common.Plugins
/// <summary>
/// Gets the Plugins.
/// </summary>
- IList<LocalPlugin> Plugins { get; }
+ IReadOnlyList<LocalPlugin> Plugins { get; }
/// <summary>
/// Creates the plugins.
@@ -45,6 +46,15 @@ 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>
+ /// <returns>True if successful.</returns>
+ Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path);
+
+ /// <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..12a1ad1ec 100644
--- a/MediaBrowser.Common/Plugins/LocalPlugin.cs
+++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs
@@ -22,7 +22,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 +59,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/Progress/ActionableProgress.cs b/MediaBrowser.Common/Progress/ActionableProgress.cs
index d5bcd5be9..fe7cb1078 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;
diff --git a/MediaBrowser.Common/Progress/SimpleProgress.cs b/MediaBrowser.Common/Progress/SimpleProgress.cs
index d75675bf1..988d8ad34 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;
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index 935a79031..142cebd0c 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -61,7 +61,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/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
index f6d3cd6cc..20fad4cb0 100644
--- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
@@ -1,6 +1,8 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Entities.Audio
{
@@ -23,15 +25,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/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 7598b29e6..53d45261e 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1243,7 +1243,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- return string.Join("/", terms.ToArray());
+ return string.Join('/', terms.ToArray());
}
/// <summary>
@@ -2795,7 +2795,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)
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 65fd1654c..76b6d39a9 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -26,7 +26,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class CollectionFolder : Folder, ICollectionFolder
{
- private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+ private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public static IXmlSerializer XmlSerializer { get; set; }
public static IServerApplicationHost ApplicationHost { get; set; }
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 1a379074d..a410c1b66 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -107,7 +107,7 @@ namespace MediaBrowser.Controller.Entities.TV
return key;
}
- return key + "-" + string.Join("-", folders);
+ return key + "-" + string.Join('-', folders);
}
private static string GetUniqueSeriesKey(BaseItem series)
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 92b2d43ce..20bfa697e 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -53,6 +53,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/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 6700761fc..80fcf71f3 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -542,7 +542,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);
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
index 1c90bb4e0..6e79dc8dd 100644
--- a/MediaBrowser.Controller/Library/NameExtensions.cs
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -10,6 +10,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 +23,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/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 6b1c096ac..8c68b47dd 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -34,6 +34,8 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
+ <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
@@ -47,14 +49,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/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index fa9222023..09080e7b2 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -541,6 +541,8 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append(encodingOptions.VaapiDevice)
.Append(' ');
}
+
+ arg.Append("-autorotate 0 ");
}
if (state.IsVideoRequest
@@ -585,6 +587,8 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append("-init_hw_device qsv@va ")
.Append("-hwaccel_output_format vaapi ");
}
+
+ arg.Append("-autorotate 0 ");
}
}
@@ -592,13 +596,13 @@ namespace MediaBrowser.Controller.MediaEncoding
&& string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
&& isNvdecDecoder)
{
- arg.Append("-hwaccel_output_format cuda ");
+ arg.Append("-hwaccel_output_format cuda -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)
@@ -1387,7 +1391,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(" ", "", StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
{
var currentScore = GetVideoProfileScore(videoStream.Profile);
var requestedScore = GetVideoProfileScore(requestedProfile);
@@ -1716,7 +1721,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (filters.Count > 0)
{
- return " -af \"" + string.Join(",", filters) + "\"";
+ return " -af \"" + string.Join(',', filters) + "\"";
}
return string.Empty;
@@ -2894,7 +2899,7 @@ namespace MediaBrowser.Controller.MediaEncoding
output += string.Format(
CultureInfo.InvariantCulture,
"{0}",
- string.Join(",", filters));
+ string.Join(',', filters));
}
return output;
@@ -2920,7 +2925,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (threads <= 0)
{
return 0;
- }
+ }
else if (threads >= Environment.ProcessorCount)
{
return Environment.ProcessorCount;
@@ -3870,7 +3875,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/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/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
index 67acdd9a3..25128a5cd 100644
--- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- protected virtual T Resolve(ItemResolveArgs args)
+ public virtual T Resolve(ItemResolveArgs args)
{
return null;
}
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..2d3b2d889 100644
--- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
@@ -39,7 +39,7 @@ 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);
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..7ad8c24e8 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);
}
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..5f620634f 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -1275,8 +1275,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/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index 396206658..dfbce5f49 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)
@@ -420,20 +420,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 +538,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/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index 4a54b677d..ef9943722 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -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/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index c0b6cf28b..47cf020b4 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -86,7 +86,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_localization = localization;
_encodingHelperFactory = encodingHelperFactory;
_startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty;
- _jsonSerializerOptions = JsonDefaults.GetOptions();
+ _jsonSerializerOptions = JsonDefaults.Options;
}
/// <inheritdoc />
@@ -103,7 +103,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 +118,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 +177,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 +209,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_ffmpegPath = path;
EncoderLocation = location;
+ return true;
}
else
{
@@ -447,7 +448,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 +571,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 +590,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 +619,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 +685,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogError(msg);
- throw new Exception(msg);
+ throw new FfmpegException(msg);
}
return tempExtractPath;
@@ -769,30 +732,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 +810,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..39fb0b47c 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -10,6 +10,8 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -24,18 +26,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/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index bd026bce1..75067315f 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -640,7 +640,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 +681,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))
{
@@ -1496,19 +1496,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..8219aa7b4 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
@@ -1,130 +1,21 @@
-#pragma warning disable CS1591
+#nullable enable
-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)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AssParser"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ public AssParser(ILogger logger) : base(logger)
{
- 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)
- {
- 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/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
index ccef7eeea..19fb951dc 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
@@ -1,102 +1,21 @@
-#pragma warning disable CS1591
+#nullable enable
-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..36dc2e01f 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
@@ -1,477 +1,21 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
+#nullable enable
+
+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..82ec6ca21
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -0,0 +1,63 @@
+#nullable enable
+
+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..39bec8da1 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -25,9 +25,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 +41,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
new ConcurrentDictionary<string, SemaphoreSlim>();
public SubtitleEncoder(
- ILibraryManager libraryManager,
ILogger<SubtitleEncoder> logger,
IApplicationPaths appPaths,
IFileSystem fileSystem,
@@ -50,7 +48,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
IHttpClientFactory httpClientFactory,
IMediaSourceManager mediaSourceManager)
{
- _libraryManager = libraryManager;
_logger = logger;
_appPaths = appPaths;
_fileSystem = fileSystem;
@@ -168,33 +165,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles
MediaStream subtitleStream,
CancellationToken cancellationToken)
{
- var inputFile = mediaSource.Path;
+ var fileInfo = await GetReadableFile(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
- var protocol = mediaSource.Protocol;
- if (subtitleStream.IsExternal)
- {
- protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path);
- }
-
- 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 +193,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 +228,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);
@@ -255,13 +242,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (GetReader(currentFormat, false) == null)
{
// 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);
}
@@ -279,12 +271,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
{
- return new SsaParser();
+ return new SsaParser(_logger);
}
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
{
- return new AssParser();
+ return new AssParser(_logger);
}
if (throwIfMissing)
@@ -492,7 +484,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 +496,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 +504,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 +519,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!File.Exists(outputPath))
{
await ExtractTextSubtitleInternal(
- _mediaEncoder.GetInputArgument(inputFile, mediaSource),
+ _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource),
subtitleStreamIndex,
outputCodec,
outputPath,
@@ -647,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_logger.LogError(msg);
- throw new Exception(msg);
+ throw new FfmpegException(msg);
}
else
{
@@ -687,7 +677,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 +686,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 +702,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);
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..a9b280301 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 = true;
+ 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/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..d83c8f2f3 100644
--- a/MediaBrowser.Model/Dlna/ContainerProfile.cs
+++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs
@@ -9,6 +9,11 @@ namespace MediaBrowser.Model.Dlna
{
public class ContainerProfile
{
+ public ContainerProfile()
+ {
+ Conditions = Array.Empty<ProfileCondition>();
+ }
+
[XmlAttribute("type")]
public DlnaProfileType Type { get; set; }
@@ -17,11 +22,6 @@ namespace MediaBrowser.Model.Dlna
[XmlAttribute("container")]
public string Container { get; set; }
- public ContainerProfile()
- {
- Conditions = Array.Empty<ProfileCondition>();
- }
-
public string[] GetContainers()
{
return SplitValue(Container);
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 50e3374f7..ec106f105 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -81,13 +81,13 @@ 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,
@@ -150,16 +150,18 @@ 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));
+ // {
+ // 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,
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..f61b8d59e 100644
--- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
+++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
@@ -57,7 +57,6 @@ namespace MediaBrowser.Model.Dlna
string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) ||
string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase))
{
-
return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType);
}
@@ -323,7 +322,6 @@ namespace MediaBrowser.Model.Dlna
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))
@@ -479,7 +477,9 @@ namespace MediaBrowser.Model.Dlna
{
if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) ||
string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase))
+ {
return ResolveImageJPGFormat(width, height);
+ }
if (string.Equals(container, "png", StringComparison.OrdinalIgnoreCase))
{
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..bf33691c7 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;
}
@@ -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;
}
@@ -698,7 +698,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)
{
@@ -1404,7 +1404,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 +1735,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));
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 55b12ae81..29da5d9e7 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,463 @@ 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;
+ }
+
+ // 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(" ", 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/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..7f18b4502 100644
--- a/MediaBrowser.Model/Dto/NameIdPair.cs
+++ b/MediaBrowser.Model/Dto/NameIdPair.cs
@@ -19,11 +19,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..ade9d7e8d 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
@@ -108,11 +108,11 @@ namespace MediaBrowser.Model.Entities
}
}
- 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 +154,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))
@@ -211,7 +211,7 @@ namespace MediaBrowser.Model.Entities
return result.ToString();
}
- return string.Join(" ", attributes);
+ return string.Join(' ', attributes);
}
case MediaStreamType.Subtitle:
@@ -229,17 +229,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 +266,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,6 +426,96 @@ 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;
@@ -533,35 +562,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..09d14dc6a 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,13 +118,7 @@ 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
{
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/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/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index dc6549787..ef08ecec6 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -155,13 +155,16 @@ 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);
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..7d4bbb2d0
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
@@ -0,0 +1,38 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using System;
+using MediaBrowser.Model.Dto;
+
+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..f622a042a 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>
@@ -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..a268a4fa6 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>
@@ -69,16 +80,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..b3db57b6d 100644
--- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
+++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -7,11 +8,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..94bb5d6e3 100644
--- a/MediaBrowser.Model/Notifications/NotificationOptions.cs
+++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs
@@ -2,18 +2,16 @@
#pragma warning disable CS1591
using System;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Model.Extensions;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Model.Notifications
{
public class NotificationOptions
{
- public NotificationOption[] Options { get; set; }
-
public NotificationOptions()
{
Options = new[]
@@ -71,6 +69,8 @@ namespace MediaBrowser.Model.Notifications
};
}
+ public NotificationOption[] Options { get; set; }
+
public NotificationOption GetOptions(string type)
{
foreach (NotificationOption i in Options)
@@ -104,7 +104,7 @@ namespace MediaBrowser.Model.Notifications
NotificationOption opt = GetOptions(type);
return opt != null && opt.Enabled &&
- !opt.DisabledMonitorUsers.Contains(userId.ToString(""), StringComparer.OrdinalIgnoreCase);
+ !opt.DisabledMonitorUsers.Contains(userId.ToString(string.Empty), 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/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
index 13b1a0dcb..56a7f3320 100644
--- a/MediaBrowser.Model/Querying/EpisodeQuery.cs
+++ b/MediaBrowser.Model/Querying/EpisodeQuery.cs
@@ -7,6 +7,11 @@ namespace MediaBrowser.Model.Querying
{
public class EpisodeQuery
{
+ public EpisodeQuery()
+ {
+ Fields = Array.Empty<ItemFields>();
+ }
+
/// <summary>
/// Gets or sets the user identifier.
/// </summary>
@@ -66,10 +71,5 @@ namespace MediaBrowser.Model.Querying
/// </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
index 1c8875890..b800f5de5 100644
--- a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs
+++ b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs
@@ -7,6 +7,13 @@ namespace MediaBrowser.Model.Querying
{
public class MovieRecommendationQuery
{
+ public MovieRecommendationQuery()
+ {
+ ItemLimit = 10;
+ CategoryLimit = 6;
+ Fields = Array.Empty<ItemFields>();
+ }
+
/// <summary>
/// Gets or sets the user identifier.
/// </summary>
@@ -36,12 +43,5 @@ namespace MediaBrowser.Model.Querying
/// </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..0555afc00 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -8,6 +8,13 @@ namespace MediaBrowser.Model.Querying
{
public class NextUpQuery
{
+ public NextUpQuery()
+ {
+ EnableImageTypes = Array.Empty<ImageType>();
+ EnableTotalRecordCount = true;
+ DisableFirstEpisode = false;
+ }
+
/// <summary>
/// Gets or sets the user id.
/// </summary>
@@ -27,19 +34,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; }
@@ -68,12 +75,5 @@ namespace MediaBrowser.Model.Querying
/// Gets or sets a value indicating whether do disable sending first episode as next up.
/// </summary>
public bool DisableFirstEpisode { get; set; }
-
- public NextUpQuery()
- {
- EnableImageTypes = Array.Empty<ImageType>();
- EnableTotalRecordCount = true;
- DisableFirstEpisode = false;
- }
}
}
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
index eb6239460..2cf0f0d5f 100644
--- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
+++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
@@ -8,6 +8,11 @@ namespace MediaBrowser.Model.Querying
{
public class UpcomingEpisodesQuery
{
+ public UpcomingEpisodesQuery()
+ {
+ EnableImageTypes = Array.Empty<ImageType>();
+ }
+
/// <summary>
/// Gets or sets the user id.
/// </summary>
@@ -21,19 +26,19 @@ namespace MediaBrowser.Model.Querying
public string ParentId { 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; }
@@ -55,10 +60,5 @@ namespace MediaBrowser.Model.Querying
/// </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/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/SyncJob.cs b/MediaBrowser.Model/Sync/SyncJob.cs
index b9290b6e8..3e396e5d1 100644
--- a/MediaBrowser.Model/Sync/SyncJob.cs
+++ b/MediaBrowser.Model/Sync/SyncJob.cs
@@ -7,6 +7,11 @@ namespace MediaBrowser.Model.Sync
{
public class SyncJob
{
+ public SyncJob()
+ {
+ RequestedItemIds = Array.Empty<Guid>();
+ }
+
/// <summary>
/// Gets or sets the identifier.
/// </summary>
@@ -126,10 +131,5 @@ namespace MediaBrowser.Model.Sync
public string PrimaryImageItemId { get; set; }
public string PrimaryImageTag { get; set; }
-
- public SyncJob()
- {
- RequestedItemIds = Array.Empty<Guid>();
- }
}
}
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..cbd60cca1 100644
--- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs
+++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
@@ -21,6 +21,10 @@ namespace MediaBrowser.Model.Tasks
/// <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/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..5bb85be7b 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;
@@ -263,7 +261,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/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 913f14d9b..d581dd434 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -242,6 +242,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 +870,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;
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 071a149db..152ea664a 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -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/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
index cd9e47743..2adb11908 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
@@ -22,7 +22,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..00feeec1f 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
@@ -29,7 +29,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 +171,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..b8095ff04 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
@@ -22,7 +22,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..59ecbc017 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
@@ -31,7 +31,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 +155,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/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index ef7933b1a..e5ad0f3e0 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -756,7 +756,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..d35805a84 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -39,7 +39,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 f6926d680..4963777bc 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -7,6 +7,8 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using TMDbLib.Objects.Find;
+using TMDbLib.Objects.Search;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -43,64 +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.Year ?? 0, 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)
@@ -113,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)
{
@@ -121,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>();
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index 3b7a0b254..d92336624 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, 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..b455e5634 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -111,10 +111,14 @@ 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))
@@ -122,14 +126,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId);
}
- item.PremiereDate = episodeResult.AirDate;
- item.ProductionYear = episodeResult.AirDate?.Year;
-
- item.Name = episodeResult.Name;
- item.Overview = episodeResult.Overview;
-
- item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage);
-
if (episodeResult.Videos?.Results != null)
{
foreach (var video in episodeResult.Videos.Results)
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..a96fc8ed6 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -59,8 +59,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 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..496e1ae25 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
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 2dc5cd55d..bf0f027fc 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -278,9 +278,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 +292,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..15a44c7ed 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,6 +46,17 @@ 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>
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index 90e13f12f..5fcf6d9aa 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -144,7 +144,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);
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 47e9d5ee8..1f3d9acff 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -205,12 +205,30 @@ namespace MediaBrowser.Providers.Subtitles
if (saveInMediaFolder)
{
- savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
+ 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))
+ {
+ 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()))
+ {
+ savePaths.Add(internalPath);
+ }
- await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
+ 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.");
+ }
}
}
@@ -228,7 +246,8 @@ namespace MediaBrowser.Providers.Subtitles
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
- using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, FileStreamBufferSize, true))
+ // 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);
}
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 18b4ffac9..3a13f3888 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -1230,11 +1230,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[] { '|', ';' };
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index 7ae55c791..6b1607530 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -45,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))
{
@@ -79,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/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
index b8f76f31d..8574be3f3 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
@@ -23,7 +23,7 @@ namespace MediaBrowser.XbmcMetadata.Providers
private readonly IUserDataManager _userDataManager;
private readonly IDirectoryService _directoryService;
- public BaseVideoNfoProvider(
+ protected BaseVideoNfoProvider(
ILogger<BaseVideoNfoProvider<T>> logger,
IFileSystem fileSystem,
IConfigurationManager config,
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/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs
index bfad6de97..0cdc5ce3d 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();
@@ -353,7 +356,7 @@ namespace Rssdp.Infrastructure
{
return;
}
-
+
var location = GetFirstHeaderUriValue("Location", message);
if (location != null)
{
@@ -515,11 +518,6 @@ namespace Rssdp.Infrastructure
private void RemoveExpiredDevicesFromCache()
{
- if (this.IsDisposed)
- {
- return;
- }
-
DiscoveredSsdpDevice[] expiredDevices = null;
lock (_Devices)
{
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/jellyfin.ruleset b/jellyfin.ruleset
index 371f02566..b012d2b00 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -32,11 +32,17 @@
<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 CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
@@ -47,6 +53,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 +63,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 -->
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/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index eca3df79b..577b61d02 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.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>
@@ -18,27 +20,23 @@
<PackageReference Include="AutoFixture.Xunit2" Version="4.15.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.3" />
<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.9.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="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/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..017a67e9f 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -10,18 +10,19 @@
<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.9.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" />
@@ -32,8 +33,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/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/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..fd77694b3
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
@@ -0,0 +1,39 @@
+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);
+ }
+ }
+} \ No newline at end of file
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.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index c766c5445..6dec25aa4 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -10,18 +10,19 @@
<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.9.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 +32,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/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 52a9e1193..5d52f94c0 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -5,18 +5,19 @@
<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.9.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 +27,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..4cc1d37ee 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.9.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" />
@@ -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/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/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/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/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 64d51e063..0c7e262f5 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -5,18 +5,19 @@
<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.9.1" />
<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" />
</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 +27,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/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index b3257ace3..ad63adadc 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -10,7 +10,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 +36,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..cc12a99a6 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.9.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>
<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/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
index fde06c5a1..a720bdade 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
@@ -7,18 +7,13 @@ namespace Jellyfin.Naming.Tests.Video
{
public sealed class CleanStringTests
{
- private readonly NamingOptions _namingOptions = new NamingOptions();
+ private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
[Theory]
[InlineData("Super movie 480p.mp4", "Super movie")]
[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, 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, out ReadOnlySpan<char> newName));
+ Assert.True(newName.IsEmpty);
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index bc5e6fa63..6e803593e 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -295,12 +295,9 @@ namespace Jellyfin.Naming.Tests.Video
FullName = i
}).ToList()).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]
@@ -369,6 +366,44 @@ namespace Jellyfin.Naming.Tests.Video
}
[Fact]
+ public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName()
+ {
+ var files = new[]
+ {
+ @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv",
+ @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Empty(result[0].Extras);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ [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()).ToList();
+
+ Assert.Equal(2, result.Count);
+ }
+
+ [Fact]
public void TestEmptyList()
{
var result = _videoListResolver.Resolve(new List<FileSystemMetadata>()).ToList();
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index ba5eaf1af..9bbbe2970 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -11,7 +11,7 @@ namespace Jellyfin.Naming.Tests.Video
{
private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
- public static IEnumerable<object[]> GetResolveFileTestData()
+ public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
{
yield return new object[]
{
@@ -156,7 +156,7 @@ 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);
diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index d77645cd9..a76c0e9a0 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -8,32 +8,34 @@
<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.9.1" />
<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="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/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index b7c1510d2..c3469035e 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -1,46 +1,18 @@
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)
{
var configManager = new Mock<IConfigurationManager>
@@ -52,15 +24,20 @@ 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.
+ [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", "[]")]
+ // 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]")]
public void IgnoreVirtualInterfaces(string interfaces, string lan, string value)
{
var conf = new NetworkConfiguration()
@@ -118,13 +95,34 @@ namespace Jellyfin.Networking.Tests
[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 void ValidIPStrings(string address)
+ public void ValidHostStrings(string address)
{
- Assert.True(TryParse(address, out _));
+ Assert.True(IPHost.TryParse(address, out _));
}
+ /// <summary>
+ /// Checks IP address formats.
+ /// </summary>
+ /// <param name="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 void ValidIPStrings(string address)
+ {
+ Assert.True(IPNetAddress.TryParse(address, out _));
+ }
/// <summary>
/// All should be invalid address strings.
@@ -138,10 +136,10 @@ namespace Jellyfin.Networking.Tests
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
public void InvalidAddressString(string address)
{
- Assert.False(TryParse(address, out _));
+ Assert.False(IPNetAddress.TryParse(address, out _));
+ Assert.False(IPHost.TryParse(address, out _));
}
-
/// <summary>
/// Test collection parsing.
/// </summary>
@@ -152,19 +150,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 +173,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,12 +196,12 @@ 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.
@@ -208,7 +210,7 @@ namespace Jellyfin.Networking.Tests
conf.EnableIPV6 = false;
nm.UpdateSettings(conf);
-
+
// Test IP4 included.
nc = nm.CreateIPCollection(settings.Split(","), false);
Assert.Equal(nc.AsString(), result2);
@@ -252,7 +254,6 @@ namespace Jellyfin.Networking.Tests
throw new ArgumentNullException(nameof(result));
}
-
var conf = new NetworkConfiguration()
{
EnableIPV6 = true,
@@ -333,8 +334,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));
}
@@ -378,7 +379,6 @@ namespace Jellyfin.Networking.Tests
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")]
@@ -455,7 +455,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 +468,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 +479,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)
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 c3b3155fe..c3c258b68 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>
@@ -22,16 +24,15 @@
<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="Microsoft.NET.Test.Sdk" Version="16.9.1" />
+ <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" />
@@ -42,8 +43,4 @@
<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.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
new file mode 100644
index 000000000..876519215
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -0,0 +1,65 @@
+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 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,
+ Path = "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,
+ Path = "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..e5508243f 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -24,5 +24,35 @@ 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")]
+ 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/HdHomerunManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs
new file mode 100644
index 000000000..7e04a1ec1
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs
@@ -0,0 +1,58 @@
+using System;
+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(expected.Length, len);
+ Assert.True(expected.SequenceEqual(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(expected.Length, len);
+ Assert.True(expected.SequenceEqual(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(expected.Length, len);
+ Assert.True(expected.SequenceEqual(buffer.Slice(0, len)));
+ }
+ }
+}
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..4fa64d8a2
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.IO;
+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()
+ {
+ IList<PackageInfo> packages = await _installationManager.GetPackages(
+ "Jellyfin Stable",
+ "https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
+ false);
+
+ Assert.Equal(25, packages.Count);
+ }
+ }
+}
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/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/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
new file mode 100644
index 000000000..34cef9005
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -0,0 +1,40 @@
+<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.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="Microsoft.Extensions.Options" Version="5.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <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>
+
+ <!-- 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..3cbd638f9 100644
--- a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
@@ -6,7 +6,7 @@ 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..4e5d0fcb6
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Reflection;
+using Emby.Server.Implementations;
+using Jellyfin.Server;
+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.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
new file mode 100644
index 000000000..a310b0ea9
--- /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.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="Microsoft.Extensions.Options" Version="5.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <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..0b714e80a 100644
--- a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs
+++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
-namespace Jellyfin.Api.Tests
+namespace Jellyfin.Server.Tests
{
public class ParseNetworkTests
{
@@ -37,28 +37,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.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index aed3e8ac5..9380fe2af 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.9.1" />
+ <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/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index 438684473..9ad093a2b 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -49,7 +49,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<Episode>()
{
@@ -105,6 +105,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 4c556b4d6..d4ce74132 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -77,7 +77,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<Video>()
{
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
index 29de2a95e..2129f3422 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
@@ -48,7 +48,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[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 e54b35eb5..3d8e13e96 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
@@ -47,7 +47,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<MusicArtist>()
{
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
index c497d4fa2..0e61fa2a1 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
@@ -43,7 +43,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[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 7c7d698f0..bdedae205 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
@@ -35,7 +35,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[Fact]
- public void Fetch_Valid_Succes()
+ public void Fetch_Valid_Success()
{
var result = new MetadataResult<Series>()
{
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>