aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-abi.yml3
-rw-r--r--.ci/azure-pipelines-main.yml10
-rw-r--r--.ci/azure-pipelines-package.yml7
-rw-r--r--.ci/azure-pipelines-test.yml5
-rw-r--r--.ci/azure-pipelines.yml5
-rw-r--r--.github/workflows/codeql-analysis.yml4
-rw-r--r--.vscode/launch.json4
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Dockerfile14
-rw-r--r--Dockerfile.arm15
-rw-r--r--Dockerfile.arm6414
-rw-r--r--DvdLib/DvdLib.csproj2
-rw-r--r--DvdLib/Ifo/Dvd.cs3
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs28
-rw-r--r--Emby.Dlna/DlnaManager.cs21
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj5
-rw-r--r--Emby.Dlna/Eventing/DlnaEventManager.cs11
-rw-r--r--Emby.Dlna/PlayTo/Device.cs12
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs8
-rw-r--r--Emby.Dlna/PlayTo/SsdpHttpClient.cs16
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs8
-rw-r--r--Emby.Dlna/Service/BaseService.cs8
-rw-r--r--Emby.Drawing/Emby.Drawing.csproj3
-rw-r--r--Emby.Drawing/ImageProcessor.cs2
-rw-r--r--Emby.Naming/Common/NamingOptions.cs6
-rw-r--r--Emby.Naming/Emby.Naming.csproj3
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs6
-rw-r--r--Emby.Notifications/Emby.Notifications.csproj2
-rw-r--r--Emby.Photos/Emby.Photos.csproj2
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs9
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs60
-rw-r--r--Emby.Server.Implementations/Archiving/ZipClient.cs5
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs2
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs54
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs53
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs6
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs508
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs7
-rw-r--r--Emby.Server.Implementations/Data/SynchronouseMode.cs30
-rw-r--r--Emby.Server.Implementations/Data/TempStoreMode.cs23
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs21
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs2
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj25
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs6
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs36
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs22
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs7
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs4
-rw-r--r--Emby.Server.Implementations/Images/BaseFolderImageProvider.cs67
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs69
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs41
-rw-r--r--Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs19
-rw-r--r--Emby.Server.Implementations/Images/MusicGenreImageProvider.cs59
-rw-r--r--Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs19
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs4
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs6
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs12
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs2
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs35
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs22
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs27
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs2
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs15
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs25
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs99
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs9
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs17
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs18
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs9
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs20
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs49
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs21
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs103
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs21
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs9
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs18
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs35
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs69
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs69
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs38
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs139
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs51
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/cy.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json94
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json118
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json75
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur_PK.json9
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs3
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs8
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs (renamed from Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs)18
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs20
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs28
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs14
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs26
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs6
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs3
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs29
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs33
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs5
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs30
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/CriticRatingComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/DateCreatedComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/NameComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs36
-rw-r--r--Emby.Server.Implementations/Sorting/PremiereDateComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/ProductionYearComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/RandomComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs12
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs4
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs2
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs6
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs6
-rw-r--r--Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs4
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs4
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs12
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs14
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs4
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs6
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs4
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs32
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs6
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs6
-rw-r--r--Jellyfin.Api/Controllers/TimeSyncController.cs4
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs10
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs2
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs20
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs27
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs2
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs3
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs2
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileCopier.cs187
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs49
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs3
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs16
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj11
-rw-r--r--Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs5
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs5
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamState.cs5
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj4
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj7
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfiguration.cs5
-rw-r--r--Jellyfin.Networking/Jellyfin.Networking.csproj2
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj10
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs4
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs12
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs5
-rw-r--r--Jellyfin.Server/Configuration/CorsPolicyProvider.cs4
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs5
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs144
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj12
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs6
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs11
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs6
-rw-r--r--Jellyfin.Server/Program.cs12
-rw-r--r--Jellyfin.Server/Startup.cs6
-rw-r--r--MediaBrowser.Common/FfmpegException.cs (renamed from MediaBrowser.MediaEncoding/FfmpegException.cs)2
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj6
-rw-r--r--MediaBrowser.Controller/Dlna/IDlnaManager.cs2
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs2
-rw-r--r--MediaBrowser.Controller/Drawing/ImageStream.cs9
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs79
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs4
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs6
-rw-r--r--MediaBrowser.Controller/Library/IDirectStreamProvider.cs19
-rw-r--r--MediaBrowser.Controller/Library/ILiveStream.cs3
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs24
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj15
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs42
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs3
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs15
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs124
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs4
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj2
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs21
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs24
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs73
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj8
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs61
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs32
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs4
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs1
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs2
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs5
-rw-r--r--MediaBrowser.Model/IO/AsyncFile.cs25
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj11
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs15
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs24
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs48
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs3
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj8
-rw-r--r--MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs220
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs1
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs8
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs1
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs47
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs1
-rw-r--r--MediaBrowser.Providers/Studios/StudiosImageProvider.cs9
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs25
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj2
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs27
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs10
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs11
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs18
-rw-r--r--README.md2
-rw-r--r--RSSDP/DisposableManagedObjectBase.cs5
-rw-r--r--RSSDP/RSSDP.csproj2
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs2
-rw-r--r--RSSDP/SsdpDevice.cs13
-rw-r--r--RSSDP/SsdpDeviceLocator.cs2
-rw-r--r--RSSDP/SsdpDevicePublisher.cs15
-rwxr-xr-xbump_version9
-rw-r--r--debian/control2
-rw-r--r--deployment/Dockerfile.centos.amd6410
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm6410
-rw-r--r--deployment/Dockerfile.debian.armhf10
-rw-r--r--deployment/Dockerfile.docker.amd644
-rw-r--r--deployment/Dockerfile.docker.arm644
-rw-r--r--deployment/Dockerfile.docker.armhf4
-rw-r--r--deployment/Dockerfile.fedora.amd649
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.linux.amd64-musl2
-rw-r--r--deployment/Dockerfile.linux.arm642
-rw-r--r--deployment/Dockerfile.linux.armhf2
-rw-r--r--deployment/Dockerfile.macos2
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd644
-rw-r--r--deployment/Dockerfile.ubuntu.arm644
-rw-r--r--deployment/Dockerfile.ubuntu.armhf4
-rw-r--r--deployment/Dockerfile.windows.amd642
-rwxr-xr-xdeployment/build.centos.amd6419
-rwxr-xr-xdeployment/build.debian.amd644
-rwxr-xr-xdeployment/build.debian.arm644
-rwxr-xr-xdeployment/build.debian.armhf4
-rwxr-xr-xdeployment/build.fedora.amd6419
-rwxr-xr-xdeployment/build.ubuntu.amd644
-rwxr-xr-xdeployment/build.ubuntu.arm644
-rwxr-xr-xdeployment/build.ubuntu.armhf4
-rw-r--r--fedora/jellyfin.spec2
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj13
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs2
-rw-r--r--src/Jellyfin.Extensions/ReadOnlyListExtension.cs61
-rw-r--r--src/Jellyfin.Extensions/ShuffleExtensions.cs4
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs34
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs32
-rw-r--r--tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs26
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj6
-rw-r--r--tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs54
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj2
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj2
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj2
-rw-r--r--tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs38
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj2
-rw-r--r--tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs4
-rw-r--r--tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs23
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs28
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj8
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs74
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs22
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs12
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs83
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_mp4_metadata.json260
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt11
-rw-r--r--tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs102
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs27
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs66
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs117
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj2
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs2
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj2
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs216
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs14
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs176
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs100
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs59
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs240
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs34
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs170
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/empty.zipbin0 -> 162 bytes
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs33
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs141
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs1
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj8
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj8
-rw-r--r--tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs3
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj2
380 files changed, 4823 insertions, 3031 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index e58a2bdc7..31f861f63 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
- default: 5.0.302
+ default: 6.0.x
jobs:
- job: CompatibilityCheck
@@ -34,6 +34,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Install ABI CompatibilityChecker Tool'
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
index d2c087c14..1086d51d2 100644
--- a/.ci/azure-pipelines-main.yml
+++ b/.ci/azure-pipelines-main.yml
@@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
- DotNetSdkVersion: 5.0.302
+ DotNetSdkVersion: 6.0.x
jobs:
- job: Build
@@ -54,6 +54,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Publish Server'
@@ -91,3 +92,10 @@ jobs:
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
+
+ - task: PublishPipelineArtifact@1
+ displayName: 'Publish Artifact Extensions'
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
+ artifactName: 'Jellyfin.Extensions'
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 543fd7fc6..4abe52b43 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -195,10 +195,11 @@ jobs:
steps:
- task: UseDotNet@2
- displayName: 'Use .NET 5.0 sdk'
+ displayName: 'Use .NET 6.0 sdk'
inputs:
packageType: 'sdk'
- version: '5.0.x'
+ version: '6.0.x'
+ includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
@@ -211,6 +212,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
@@ -225,6 +227,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 7ec4cdad1..80a5732ee 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
- default: 5.0.302
+ default: 6.0.x
jobs:
- job: Test
@@ -41,6 +41,7 @@ jobs:
inputs:
packageType: sdk
version: ${{ parameters.DotNetSdkVersion }}
+ includePreviewVersions: true
- task: SonarCloudPrepare@1
displayName: 'Prepare analysis on SonarCloud'
@@ -94,5 +95,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
- targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
+ targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 4e8b6557b..19c9caacb 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -5,8 +5,6 @@ variables:
value: 'tests/**/*Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
-- name: DotNetSdkVersion
- value: 5.0.302
pr:
autoCancel: true
@@ -57,6 +55,9 @@ jobs:
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
+ Extensions:
+ NugetPackageName: Jellyfin.Extensions
+ AssemblyFileName: Jellyfin.Extensions.dll
LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 3e456f909..e07d913b5 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -24,7 +24,9 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: '5.0.x'
+ dotnet-version: '6.0.x'
+ include-prerelease: true
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e55ea2248..b82956a72 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index cb52cafed..5c4a031bc 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -149,6 +149,7 @@
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
+ - [MBR-0001](https://github.com/MBR-0001)
# Emby Contributors
diff --git a/Dockerfile b/Dockerfile
index 791a6113e..e133c0819 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
@@ -12,7 +12,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-FROM debian:bullseye-slim as app
+FROM debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -29,8 +29,9 @@ ARG LEVEL_ZERO_VERSION=1.2.20826
# Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
+# curl: healthcheck
RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
@@ -61,7 +62,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -76,6 +77,8 @@ RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --
FROM app
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -85,3 +88,6 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 8d4b548bc..a46fa331d 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:bullseye-slim as app
+FROM arm32v7/debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -24,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
+
+# curl: setup & healthcheck
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
@@ -42,7 +44,7 @@ RUN apt-get update \
vainfo \
libva2 \
locales \
- && apt-get remove curl gnupg -y \
+ && apt-get remove gnupg -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -50,7 +52,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -66,6 +68,8 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
FROM app
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -75,3 +79,6 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 835aa36a1..1279c47f8 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -2,7 +2,7 @@
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:bullseye-slim as app
+FROM arm64v8/debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -24,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
+
+# curl: healcheck
RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
ffmpeg \
libssl-dev \
@@ -33,6 +35,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
libomxil-bellagio0 \
libomxil-bellagio-bin \
locales \
+ curl \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -40,7 +43,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
@@ -56,6 +59,8 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
FROM app
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -65,3 +70,6 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/bin/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index b8301e2f2..755d29160 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -10,7 +10,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index b4a11ed5d..7f8ece47d 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
@@ -76,7 +77,7 @@ namespace DvdLib.Ifo
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
{
- var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
+ var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index c00078499..0a84f30c4 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -41,8 +41,6 @@ namespace Emby.Dlna.Didl
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress;
@@ -317,7 +315,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+ writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
if (filter.Contains("res@size"))
@@ -328,7 +326,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
- writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+ writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
@@ -342,7 +340,7 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+ writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (filter.Contains("res@resolution"))
@@ -361,12 +359,12 @@ namespace Emby.Dlna.Didl
if (targetSampleRate.HasValue)
{
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (totalBitrate.HasValue)
{
- writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
var mediaProfile = _profile.GetVideoMediaProfile(
@@ -552,7 +550,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+ writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
if (filter.Contains("res@size"))
@@ -563,7 +561,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
- writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+ writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
@@ -575,17 +573,17 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+ writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (targetSampleRate.HasValue)
{
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (targetAudioBitrate.HasValue)
{
- writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
var mediaProfile = _profile.GetAudioMediaProfile(
@@ -639,7 +637,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1");
- writer.WriteAttributeString("childCount", childCount.ToString(_usCulture));
+ writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
var clientId = GetClientId(folder, stubType);
@@ -931,11 +929,11 @@ namespace Emby.Dlna.Didl
if (item.IndexNumber.HasValue)
{
- AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+ AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
if (item is Episode)
{
- AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+ AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
}
}
}
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 68fc80c0a..73e8a0008 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -359,14 +359,17 @@ namespace Emby.Dlna
// The stream should exist as we just got its name from GetManifestResourceNames
using (var stream = _assembly.GetManifestResourceStream(name)!)
{
+ var length = stream.Length;
var fileInfo = _fileSystem.GetFileInfo(path);
- if (!fileInfo.Exists || fileInfo.Length != stream.Length)
+ if (!fileInfo.Exists || fileInfo.Length != length)
{
Directory.CreateDirectory(systemProfilesPath);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
+ var fileOptions = AsyncFile.WriteOptions;
+ fileOptions.Mode = FileMode.CreateNew;
+ fileOptions.PreallocationSize = length;
+ using (var fileStream = new FileStream(path, fileOptions))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -486,18 +489,22 @@ namespace Emby.Dlna
}
/// <inheritdoc />
- public ImageStream GetIcon(string filename)
+ public ImageStream? GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png
: ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
+ var stream = _assembly.GetManifestResourceStream(resource);
+ if (stream == null)
+ {
+ return null;
+ }
- return new ImageStream
+ return new ImageStream(stream)
{
- Format = format,
- Stream = _assembly.GetManifestResourceStream(resource)
+ Format = format
};
}
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 970c16d2e..c8332e44e 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -17,10 +17,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<!-- Code Analyzers-->
@@ -73,7 +72,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" />
</ItemGroup>
</Project>
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index 3c9136090..d17e23871 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -11,6 +11,7 @@ using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
@@ -25,8 +26,6 @@ namespace Emby.Dlna.Eventing
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
@@ -82,9 +81,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
- header = header.Split('-')[^1];
-
- if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return val;
}
@@ -107,7 +104,7 @@ namespace Emby.Dlna.Eventing
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
response.Headers["SID"] = subscriptionId;
- response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
+ response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
return response;
}
@@ -164,7 +161,7 @@ namespace Emby.Dlna.Eventing
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
- options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
+ options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
try
{
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 11fcd81cf..0b2288000 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
{
public class Device : IDisposable
{
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
@@ -640,7 +638,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- Volume = int.Parse(volumeValue, UsCulture);
+ Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
if (Volume > 0)
{
@@ -842,7 +840,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
- Duration = TimeSpan.Parse(duration, UsCulture);
+ Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
}
else
{
@@ -854,7 +852,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
- Position = TimeSpan.Parse(position, UsCulture);
+ Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
}
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
@@ -1194,8 +1192,8 @@ namespace Emby.Dlna.PlayTo
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
- var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
- var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
+ var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
return new DeviceIcon
{
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 0e49fd2c0..f25d8017e 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -30,8 +30,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlayToController : ISessionController, IDisposable
{
- private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
@@ -716,7 +714,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetAudioStreamIndex(val);
}
@@ -728,7 +726,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
@@ -740,7 +738,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{
- if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+ if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index f14f73bb6..cade7b4c2 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Jellyfin";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly IHttpClientFactory _httpClientFactory;
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
@@ -45,10 +43,12 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await XDocument.LoadAsync(
stream,
- LoadOptions.PreserveWhitespace,
+ LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
@@ -78,14 +78,15 @@ namespace Emby.Dlna.PlayTo
{
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
options.Headers.UserAgent.ParseAdd(USERAGENT);
- options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
- options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
+ options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
+ options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
- options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
+ options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
}
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
@@ -94,12 +95,13 @@ namespace Emby.Dlna.PlayTo
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
try
{
return await XDocument.LoadAsync(
stream,
- LoadOptions.PreserveWhitespace,
+ LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 3f3dfccd3..80a45f2b2 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -15,7 +15,6 @@ namespace Emby.Dlna.Server
{
private readonly DeviceProfile _profile;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly string _serverUdn;
private readonly string _serverAddress;
private readonly string _serverName;
@@ -193,10 +192,10 @@ namespace Emby.Dlna.Server
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
.Append("</mimetype>");
builder.Append("<width>")
- .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+ .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
.Append("</width>");
builder.Append("<height>")
- .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+ .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
.Append("</height>");
builder.Append("<depth>")
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
@@ -250,8 +249,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
- // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
- return SecurityElement.Escape(url) ?? string.Empty;
+ return SecurityElement.Escape(url);
}
private IEnumerable<DeviceIcon> GetIcons()
diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs
index a97c4d63a..68fd98758 100644
--- a/Emby.Dlna/Service/BaseService.cs
+++ b/Emby.Dlna/Service/BaseService.cs
@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
return EventManager.CancelEventSubscription(subscriptionId);
}
- public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl)
+ public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
{
- return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl);
+ return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
}
- public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl)
+ public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
{
- return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl);
+ return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
}
}
}
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index baf350c6f..149e4b5d9 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -6,10 +6,9 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<ItemGroup>
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 0ad8bca31..ac73cfa42 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -102,7 +102,7 @@ namespace Emby.Drawing
{
var file = await ProcessImage(options).ConfigureAwait(false);
- using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
+ using (var fileStream = AsyncFile.OpenRead(file.Item1))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 915ce42cc..248d1800d 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -479,6 +479,12 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.Suffix,
+ "-deletedscene",
+ MediaType.Video),
+
+ new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-featurette",
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 07d879e96..e9e9edda6 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -6,14 +6,13 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
- <AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index a32af002c..7bc226614 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -11,6 +11,7 @@ namespace Emby.Naming.Video
/// </summary>
public class ExtraResolver
{
+ private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private readonly NamingOptions _options;
/// <summary>
@@ -62,9 +63,10 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
- var filename = Path.GetFileNameWithoutExtension(pathSpan);
+ // Trim the digits from the end of the filename so we can recognize things like -trailer2
+ var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
- if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+ if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 5edcf2f29..d200682e6 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 00b2f0f94..bf6252c19 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,7 +19,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 0308a68e4..3a916e5c0 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -41,20 +41,19 @@ namespace Emby.Server.Implementations.AppBase
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
- byte[] newBytes = stream.GetBuffer();
- int newBytesLen = (int)stream.Length;
+ Span<byte> newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
// If the file didn't exist before, or if something has changed, re-save
- if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
+ if (buffer == null || !newBytes.SequenceEqual(buffer))
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
+
// Save it after load in case we got new items
- // 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);
+ fs.Write(newBytes);
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 3a504d2f4..6fd152a42 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -306,7 +306,7 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public string Name => ApplicationProductName;
- private CertificateInfo CertificateInfo { get; set; }
+ private string CertificatePath { get; set; }
public X509Certificate2 Certificate { get; private set; }
@@ -548,12 +548,8 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
- CertificateInfo = new CertificateInfo
- {
- Path = networkConfiguration.CertificatePath,
- Password = networkConfiguration.CertificatePassword
- };
- Certificate = GetCertificate(CertificateInfo);
+ CertificatePath = networkConfiguration.CertificatePath;
+ Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
RegisterServices();
@@ -729,30 +725,27 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
- private X509Certificate2 GetCertificate(CertificateInfo info)
+ private X509Certificate2 GetCertificate(string path, string password)
{
- var certificateLocation = info?.Path;
-
- if (string.IsNullOrWhiteSpace(certificateLocation))
+ if (string.IsNullOrWhiteSpace(path))
{
return null;
}
try
{
- if (!File.Exists(certificateLocation))
+ if (!File.Exists(path))
{
return null;
}
// Don't use an empty string password
- var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
+ password = string.IsNullOrWhiteSpace(password) ? null : password;
- var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
- // localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
+ var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
if (!localCert.HasPrivateKey)
{
- Logger.LogError("No private key included in SSL cert {CertificateLocation}.", certificateLocation);
+ Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
return null;
}
@@ -760,7 +753,7 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error loading cert from {CertificateLocation}", certificateLocation);
+ Logger.LogError(ex, "Error loading cert from {CertificateLocation}", path);
return null;
}
}
@@ -882,7 +875,7 @@ namespace Emby.Server.Implementations
"http://" + i + ":" + HttpPort + "/"
};
- if (CertificateInfo != null)
+ if (Certificate != null)
{
prefixes.Add("https://" + i + ":" + HttpsPort + "/");
}
@@ -946,7 +939,7 @@ namespace Emby.Server.Implementations
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
- && !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
+ && !string.Equals(CertificatePath, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
@@ -1074,9 +1067,9 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the system status.
/// </summary>
- /// <param name="source">Where this request originated.</param>
+ /// <param name="request">Where this request originated.</param>
/// <returns>SystemInfo.</returns>
- public SystemInfo GetSystemInfo(IPAddress source)
+ public SystemInfo GetSystemInfo(HttpRequest request)
{
return new SystemInfo
{
@@ -1098,7 +1091,7 @@ namespace Emby.Server.Implementations
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(source),
+ LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
SystemArchitecture = RuntimeInformation.OSArchitecture,
PackageName = _startupOptions.PackageName
@@ -1110,7 +1103,7 @@ namespace Emby.Server.Implementations
.Select(i => new WakeOnLanInfo(i))
.ToList();
- public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
+ public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
@@ -1119,7 +1112,7 @@ namespace Emby.Server.Implementations
Id = SystemId,
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(address),
+ LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
@@ -1147,6 +1140,18 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
+ // Return the host in the HTTP request as the API url
+ if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
+ {
+ int? requestPort = request.Host.Port;
+ if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
+ {
+ requestPort = -1;
+ }
+
+ return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
+ }
+
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
@@ -1293,11 +1298,4 @@ namespace Emby.Server.Implementations
_disposed = true;
}
}
-
- internal class CertificateInfo
- {
- public string Path { get; set; }
-
- public string Password { get; set; }
- }
}
diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs
index 591ae547d..9e1d550eb 100644
--- a/Emby.Server.Implementations/Archiving/ZipClient.cs
+++ b/Emby.Server.Implementations/Archiving/ZipClient.cs
@@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Archiving
options.Overwrite = true;
}
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -58,6 +59,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -71,6 +73,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -120,6 +123,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -151,6 +155,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 6faa5d363..178f30de0 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -10,8 +10,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 4a9b28085..673810c49 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -10,8 +10,12 @@ namespace Emby.Server.Implementations.Cryptography
/// <summary>
/// Class providing abstractions over cryptographic functions.
/// </summary>
- public class CryptographyProvider : ICryptoProvider, IDisposable
+ public class CryptographyProvider : ICryptoProvider
{
+ // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
+ // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
+ // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
+ // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{
"MD5",
@@ -30,22 +34,6 @@ namespace Emby.Server.Implementations.Cryptography
"System.Security.Cryptography.SHA512"
};
- private RandomNumberGenerator _randomNumberGenerator;
-
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CryptographyProvider"/> class.
- /// </summary>
- public CryptographyProvider()
- {
- // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
- // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
- // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
- // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
- _randomNumberGenerator = RandomNumberGenerator.Create();
- }
-
/// <inheritdoc />
public string DefaultHashMethod => "PBKDF2";
@@ -101,36 +89,6 @@ namespace Emby.Server.Implementations.Cryptography
/// <inheritdoc />
public byte[] GenerateSalt(int length)
- {
- byte[] salt = new byte[length];
- _randomNumberGenerator.GetBytes(salt);
- return salt;
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _randomNumberGenerator.Dispose();
- }
-
- _disposed = true;
- }
+ => RandomNumberGenerator.GetBytes(length);
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 01c9fbca8..73c31f49d 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data
/// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get; set; }
- protected ManagedConnection GetConnection(bool _ = false)
+ protected ManagedConnection GetConnection(bool readOnly = false)
{
WriteLock.Wait();
if (WriteConnection != null)
@@ -249,55 +249,4 @@ namespace Emby.Server.Implementations.Data
_disposed = true;
}
}
-
- /// <summary>
- /// The disk synchronization mode, controls how aggressively SQLite will write data
- /// all the way out to physical storage.
- /// </summary>
- public enum SynchronousMode
- {
- /// <summary>
- /// SQLite continues without syncing as soon as it has handed data off to the operating system.
- /// </summary>
- Off = 0,
-
- /// <summary>
- /// SQLite database engine will still sync at the most critical moments.
- /// </summary>
- Normal = 1,
-
- /// <summary>
- /// SQLite database engine will use the xSync method of the VFS
- /// to ensure that all content is safely written to the disk surface prior to continuing.
- /// </summary>
- Full = 2,
-
- /// <summary>
- /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
- /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
- /// </summary>
- Extra = 3
- }
-
- /// <summary>
- /// Storage mode used by temporary database files.
- /// </summary>
- public enum TempStoreMode
- {
- /// <summary>
- /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
- /// is used to determine where temporary tables and indices are stored.
- /// </summary>
- Default = 0,
-
- /// <summary>
- /// Temporary tables and indices are stored in a file.
- /// </summary>
- File = 1,
-
- /// <summary>
- /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
- /// </summary>
- Memory = 2
- }
}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
index afc8966f9..44dad5b17 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -9,8 +9,10 @@ namespace Emby.Server.Implementations.Data
{
public class ManagedConnection : IDisposable
{
- private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
+
+ private SQLiteDatabaseConnection? _db;
+
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 3289e7609..381eb92a8 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Data
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
- DateTimeStyles.None).ToUniversalTime();
+ DateTimeStyles.AdjustToUniversal);
}
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
@@ -108,9 +108,9 @@ namespace Emby.Server.Implementations.Data
var dateText = item.ToString();
- if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
+ if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
- result = dateTimeResult.ToUniversalTime();
+ result = dateTimeResult;
return true;
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 88fc5018d..13f1df7c8 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -46,6 +46,11 @@ namespace Emby.Server.Implementations.Data
private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
+ private const string SaveItemCommandText =
+ @"replace into TypedBaseItems
+ (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
+ values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
+
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
private readonly ILocalizationManager _localization;
@@ -55,6 +60,231 @@ namespace Emby.Server.Implementations.Data
private readonly TypeMapper _typeMapper;
private readonly JsonSerializerOptions _jsonOptions;
+ private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
+
+ private static readonly string[] _retriveItemColumns =
+ {
+ "type",
+ "data",
+ "StartDate",
+ "EndDate",
+ "ChannelId",
+ "IsMovie",
+ "IsSeries",
+ "EpisodeTitle",
+ "IsRepeat",
+ "CommunityRating",
+ "CustomRating",
+ "IndexNumber",
+ "IsLocked",
+ "PreferredMetadataLanguage",
+ "PreferredMetadataCountryCode",
+ "Width",
+ "Height",
+ "DateLastRefreshed",
+ "Name",
+ "Path",
+ "PremiereDate",
+ "Overview",
+ "ParentIndexNumber",
+ "ProductionYear",
+ "OfficialRating",
+ "ForcedSortName",
+ "RunTimeTicks",
+ "Size",
+ "DateCreated",
+ "DateModified",
+ "guid",
+ "Genres",
+ "ParentId",
+ "Audio",
+ "ExternalServiceId",
+ "IsInMixedFolder",
+ "DateLastSaved",
+ "LockedFields",
+ "Studios",
+ "Tags",
+ "TrailerTypes",
+ "OriginalTitle",
+ "PrimaryVersionId",
+ "DateLastMediaAdded",
+ "Album",
+ "CriticRating",
+ "IsVirtualItem",
+ "SeriesName",
+ "SeasonName",
+ "SeasonId",
+ "SeriesId",
+ "PresentationUniqueKey",
+ "InheritedParentalRatingValue",
+ "ExternalSeriesId",
+ "Tagline",
+ "ProviderIds",
+ "Images",
+ "ProductionLocations",
+ "ExtraIds",
+ "TotalBitrate",
+ "ExtraType",
+ "Artists",
+ "AlbumArtists",
+ "ExternalId",
+ "SeriesPresentationUniqueKey",
+ "ShowId",
+ "OwnerId"
+ };
+
+ private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
+
+ private static readonly string[] _mediaStreamSaveColumns =
+ {
+ "ItemId",
+ "StreamIndex",
+ "StreamType",
+ "Codec",
+ "Language",
+ "ChannelLayout",
+ "Profile",
+ "AspectRatio",
+ "Path",
+ "IsInterlaced",
+ "BitRate",
+ "Channels",
+ "SampleRate",
+ "IsDefault",
+ "IsForced",
+ "IsExternal",
+ "Height",
+ "Width",
+ "AverageFrameRate",
+ "RealFrameRate",
+ "Level",
+ "PixelFormat",
+ "BitDepth",
+ "IsAnamorphic",
+ "RefFrames",
+ "CodecTag",
+ "Comment",
+ "NalLengthSize",
+ "IsAvc",
+ "Title",
+ "TimeBase",
+ "CodecTimeBase",
+ "ColorPrimaries",
+ "ColorSpace",
+ "ColorTransfer"
+ };
+
+ private static readonly string _mediaStreamSaveColumnsInsertQuery =
+ $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
+
+ private static readonly string _mediaStreamSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
+
+ private static readonly string[] _mediaAttachmentSaveColumns =
+ {
+ "ItemId",
+ "AttachmentIndex",
+ "Codec",
+ "CodecTag",
+ "Comment",
+ "Filename",
+ "MIMEType"
+ };
+
+ private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
+
+ private static readonly string _mediaAttachmentInsertPrefix;
+
+ private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Program",
+ "TvChannel",
+ "LiveTvProgram",
+ "LiveTvTvChannel"
+ };
+
+ private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Series",
+ "Season",
+ "MusicAlbum",
+ "MusicArtist",
+ "PhotoAlbum"
+ };
+
+ private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "TvChannel",
+ "LiveTvTvChannel"
+ };
+
+ private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Program",
+ "LiveTvProgram"
+ };
+
+ private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Book",
+ "AudioBook",
+ "Episode",
+ "Season"
+ };
+
+ private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Series",
+ "Season",
+ "PhotoAlbum"
+ };
+
+ private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Audio",
+ "MusicAlbum",
+ "MusicVideo",
+ "AudioBook",
+ "AudioPodcast"
+ };
+
+ private static readonly Type[] _knownTypes =
+ {
+ typeof(LiveTvProgram),
+ typeof(LiveTvChannel),
+ typeof(Series),
+ typeof(Audio),
+ typeof(MusicAlbum),
+ typeof(MusicArtist),
+ typeof(MusicGenre),
+ typeof(MusicVideo),
+ typeof(Movie),
+ typeof(Playlist),
+ typeof(AudioBook),
+ typeof(Trailer),
+ typeof(BoxSet),
+ typeof(Episode),
+ typeof(Season),
+ typeof(Series),
+ typeof(Book),
+ typeof(CollectionFolder),
+ typeof(Folder),
+ typeof(Genre),
+ typeof(Person),
+ typeof(Photo),
+ typeof(PhotoAlbum),
+ typeof(Studio),
+ typeof(UserRootFolder),
+ typeof(UserView),
+ typeof(Video),
+ typeof(Year),
+ typeof(Channel),
+ typeof(AggregateFolder)
+ };
+
+ private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
+
static SqliteItemRepository()
{
var queryPrefixText = new StringBuilder();
@@ -115,6 +345,8 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Opens the connection to the database.
/// </summary>
+ /// <param name="userDataRepo">The user data repository.</param>
+ /// <param name="userManager">The user manager.</param>
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{
const string CreateMediaStreamsTableCommand
@@ -154,7 +386,7 @@ namespace Emby.Server.Implementations.Data
"drop index if exists idx_TypedBaseItems",
"drop index if exists idx_mediastreams",
"drop index if exists idx_mediastreams1",
- "drop index if exists idx_"+ChaptersTableName,
+ "drop index if exists idx_" + ChaptersTableName,
"drop index if exists idx_UserDataKeys1",
"drop index if exists idx_UserDataKeys2",
"drop index if exists idx_TypeTopParentId3",
@@ -340,151 +572,12 @@ namespace Emby.Server.Implementations.Data
userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
}
- private static readonly string[] _retriveItemColumns =
- {
- "type",
- "data",
- "StartDate",
- "EndDate",
- "ChannelId",
- "IsMovie",
- "IsSeries",
- "EpisodeTitle",
- "IsRepeat",
- "CommunityRating",
- "CustomRating",
- "IndexNumber",
- "IsLocked",
- "PreferredMetadataLanguage",
- "PreferredMetadataCountryCode",
- "Width",
- "Height",
- "DateLastRefreshed",
- "Name",
- "Path",
- "PremiereDate",
- "Overview",
- "ParentIndexNumber",
- "ProductionYear",
- "OfficialRating",
- "ForcedSortName",
- "RunTimeTicks",
- "Size",
- "DateCreated",
- "DateModified",
- "guid",
- "Genres",
- "ParentId",
- "Audio",
- "ExternalServiceId",
- "IsInMixedFolder",
- "DateLastSaved",
- "LockedFields",
- "Studios",
- "Tags",
- "TrailerTypes",
- "OriginalTitle",
- "PrimaryVersionId",
- "DateLastMediaAdded",
- "Album",
- "CriticRating",
- "IsVirtualItem",
- "SeriesName",
- "SeasonName",
- "SeasonId",
- "SeriesId",
- "PresentationUniqueKey",
- "InheritedParentalRatingValue",
- "ExternalSeriesId",
- "Tagline",
- "ProviderIds",
- "Images",
- "ProductionLocations",
- "ExtraIds",
- "TotalBitrate",
- "ExtraType",
- "Artists",
- "AlbumArtists",
- "ExternalId",
- "SeriesPresentationUniqueKey",
- "ShowId",
- "OwnerId"
- };
-
- private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
-
- private static readonly string[] _mediaStreamSaveColumns =
- {
- "ItemId",
- "StreamIndex",
- "StreamType",
- "Codec",
- "Language",
- "ChannelLayout",
- "Profile",
- "AspectRatio",
- "Path",
- "IsInterlaced",
- "BitRate",
- "Channels",
- "SampleRate",
- "IsDefault",
- "IsForced",
- "IsExternal",
- "Height",
- "Width",
- "AverageFrameRate",
- "RealFrameRate",
- "Level",
- "PixelFormat",
- "BitDepth",
- "IsAnamorphic",
- "RefFrames",
- "CodecTag",
- "Comment",
- "NalLengthSize",
- "IsAvc",
- "Title",
- "TimeBase",
- "CodecTimeBase",
- "ColorPrimaries",
- "ColorSpace",
- "ColorTransfer"
- };
-
- private static readonly string _mediaStreamSaveColumnsInsertQuery =
- $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
-
- private static readonly string _mediaStreamSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
-
- private static readonly string[] _mediaAttachmentSaveColumns =
- {
- "ItemId",
- "AttachmentIndex",
- "Codec",
- "CodecTag",
- "Comment",
- "Filename",
- "MIMEType"
- };
-
- private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
-
- private static readonly string _mediaAttachmentInsertPrefix;
-
- private const string SaveItemCommandText =
- @"replace into TypedBaseItems
- (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
- values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
-
/// <summary>
/// Save a standard item in the repo.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException">item</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
public void SaveItem(BaseItem item, CancellationToken cancellationToken)
{
if (item == null)
@@ -509,7 +602,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
+ using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
@@ -526,9 +619,7 @@ namespace Emby.Server.Implementations.Data
/// <param name="items">The items.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">
- /// items
- /// or
- /// cancellationToken
+ /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
/// </exception>
public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
{
@@ -1150,7 +1241,7 @@ namespace Emby.Server.Implementations.Data
return null;
}
- if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
+ if (Enum.TryParse(imageType, true, out ImageType type))
{
image.Type = type;
}
@@ -1216,8 +1307,8 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
public BaseItem RetrieveItem(Guid id)
{
if (id == Guid.Empty)
@@ -1571,7 +1662,6 @@ namespace Emby.Server.Implementations.Data
if (reader.TryGetString(index++, out var audioString))
{
- // TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916
if (Enum.TryParse(audioString, true, out ProgramAudio audio))
{
item.Audio = audio;
@@ -1610,18 +1700,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index++, out var lockedFields))
{
- IEnumerable<MetadataField> GetLockedFields(string s)
+ List<MetadataField> fields = null;
+ foreach (var i in lockedFields.AsSpan().Split('|'))
{
- foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
+ if (Enum.TryParse(i, true, out MetadataField parsedValue))
{
- if (Enum.TryParse(i, true, out MetadataField parsedValue))
- {
- yield return parsedValue;
- }
+ (fields ??= new List<MetadataField>()).Add(parsedValue);
}
}
- item.LockedFields = GetLockedFields(lockedFields).ToArray();
+ item.LockedFields = fields?.ToArray() ?? Array.Empty<MetadataField>();
}
}
@@ -1647,18 +1735,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index, out var trailerTypes))
{
- IEnumerable<TrailerType> GetTrailerTypes(string s)
+ List<TrailerType> types = null;
+ foreach (var i in trailerTypes.AsSpan().Split('|'))
{
- foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
+ if (Enum.TryParse(i, true, out TrailerType parsedValue))
{
- if (Enum.TryParse(i, true, out TrailerType parsedValue))
- {
- yield return parsedValue;
- }
+ (types ??= new List<TrailerType>()).Add(parsedValue);
}
}
- trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray();
+ trailer.TrailerTypes = types?.ToArray() ?? Array.Empty<TrailerType>();
}
}
@@ -1991,6 +2077,8 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Saves the chapters.
/// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="chapters">The chapters.</param>
public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters)
{
CheckDisposed();
@@ -2090,8 +2178,6 @@ namespace Emby.Server.Implementations.Data
|| query.IsLiked.HasValue;
}
- private readonly ItemFields[] _allFields = Enum.GetValues<ItemFields>();
-
private bool HasField(InternalItemsQuery query, ItemFields name)
{
switch (name)
@@ -2124,23 +2210,6 @@ namespace Emby.Server.Implementations.Data
}
}
- private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Series",
- "Season",
- "MusicAlbum",
- "MusicArtist",
- "PhotoAlbum"
- };
-
- private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Program",
- "TvChannel",
- "LiveTvProgram",
- "LiveTvTvChannel"
- };
-
private bool HasProgramAttributes(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2156,12 +2225,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
}
- private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "TvChannel",
- "LiveTvTvChannel"
- };
-
private bool HasServiceName(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2177,12 +2240,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
}
- private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Program",
- "LiveTvProgram"
- };
-
private bool HasStartDate(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2218,22 +2275,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
}
- private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Series",
- "Season",
- "PhotoAlbum"
- };
-
- private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Audio",
- "MusicAlbum",
- "MusicVideo",
- "AudioBook",
- "AudioPodcast"
- };
-
private bool HasArtistFields(InternalItemsQuery query)
{
if (_artistExcludeParentTypes.Contains(query.ParentType))
@@ -2249,14 +2290,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
}
- private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Book",
- "AudioBook",
- "Episode",
- "Season"
- };
-
private bool HasSeriesFields(InternalItemsQuery query)
{
if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
@@ -2274,7 +2307,7 @@ namespace Emby.Server.Implementations.Data
private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
{
- foreach (var field in _allFields)
+ foreach (var field in _allItemFields)
{
if (!HasField(query, field))
{
@@ -4816,40 +4849,6 @@ namespace Emby.Server.Implementations.Data
return false;
}
- private static readonly Type[] _knownTypes =
- {
- typeof(LiveTvProgram),
- typeof(LiveTvChannel),
- typeof(Series),
- typeof(Audio),
- typeof(MusicAlbum),
- typeof(MusicArtist),
- typeof(MusicGenre),
- typeof(MusicVideo),
- typeof(Movie),
- typeof(Playlist),
- typeof(AudioBook),
- typeof(Trailer),
- typeof(BoxSet),
- typeof(Episode),
- typeof(Season),
- typeof(Series),
- typeof(Book),
- typeof(CollectionFolder),
- typeof(Folder),
- typeof(Genre),
- typeof(Person),
- typeof(Photo),
- typeof(PhotoAlbum),
- typeof(Studio),
- typeof(UserRootFolder),
- typeof(UserView),
- typeof(Video),
- typeof(Year),
- typeof(Channel),
- typeof(AggregateFolder)
- };
-
public void UpdateInheritedValues()
{
string sql = string.Join(
@@ -4891,9 +4890,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return dict;
}
- // Not crazy about having this all the way down here, but at least it's in one place
- private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
-
private string MapIncludeItemTypes(string value)
{
if (_types.TryGetValue(value, out string result))
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 829f1de2f..107096b5f 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -32,6 +32,9 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Opens the connection to the database.
/// </summary>
+ /// <param name="userManager">The user manager.</param>
+ /// <param name="dbLock">The lock to use for database IO.</param>
+ /// <param name="dbConnection">The connection to use for database IO.</param>
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
{
WriteLock.Dispose();
@@ -49,8 +52,8 @@ 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)",
"drop index if exists idx_userdata",
diff --git a/Emby.Server.Implementations/Data/SynchronouseMode.cs b/Emby.Server.Implementations/Data/SynchronouseMode.cs
new file mode 100644
index 000000000..cde524e2e
--- /dev/null
+++ b/Emby.Server.Implementations/Data/SynchronouseMode.cs
@@ -0,0 +1,30 @@
+namespace Emby.Server.Implementations.Data;
+
+/// <summary>
+/// The disk synchronization mode, controls how aggressively SQLite will write data
+/// all the way out to physical storage.
+/// </summary>
+public enum SynchronousMode
+{
+ /// <summary>
+ /// SQLite continues without syncing as soon as it has handed data off to the operating system.
+ /// </summary>
+ Off = 0,
+
+ /// <summary>
+ /// SQLite database engine will still sync at the most critical moments.
+ /// </summary>
+ Normal = 1,
+
+ /// <summary>
+ /// SQLite database engine will use the xSync method of the VFS
+ /// to ensure that all content is safely written to the disk surface prior to continuing.
+ /// </summary>
+ Full = 2,
+
+ /// <summary>
+ /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
+ /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
+ /// </summary>
+ Extra = 3
+}
diff --git a/Emby.Server.Implementations/Data/TempStoreMode.cs b/Emby.Server.Implementations/Data/TempStoreMode.cs
new file mode 100644
index 000000000..d2427ce47
--- /dev/null
+++ b/Emby.Server.Implementations/Data/TempStoreMode.cs
@@ -0,0 +1,23 @@
+namespace Emby.Server.Implementations.Data;
+
+/// <summary>
+/// Storage mode used by temporary database files.
+/// </summary>
+public enum TempStoreMode
+{
+ /// <summary>
+ /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
+ /// is used to determine where temporary tables and indices are stored.
+ /// </summary>
+ Default = 0,
+
+ /// <summary>
+ /// Temporary tables and indices are stored in a file.
+ /// </summary>
+ File = 1,
+
+ /// <summary>
+ /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
+ /// </summary>
+ Memory = 2
+}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index 3d15b3e76..0cfced8be 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -15,9 +15,18 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger;
-
private readonly object _syncLock = new object();
+ private string _id;
+
+ public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
+ {
+ _appPaths = appPaths;
+ _logger = loggerFactory.CreateLogger<DeviceId>();
+ }
+
+ public string Value => _id ?? (_id = GetDeviceId());
+
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
private string GetCachedId()
@@ -86,15 +95,5 @@ namespace Emby.Server.Implementations.Devices
return id;
}
-
- private string _id;
-
- public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
- {
- _appPaths = appPaths;
- _logger = loggerFactory.CreateLogger<DeviceId>();
- }
-
- public string Value => _id ?? (_id = GetDeviceId());
}
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 74400b512..ad76f3d6d 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -420,7 +420,7 @@ namespace Emby.Server.Implementations.Dto
// Just return something so that apps that are expecting a value won't think the folders are empty
if (folder is ICollectionFolder || folder is UserView)
{
- return new Random().Next(1, 10);
+ return Random.Shared.Next(1, 10);
}
return folder.GetChildCount(user);
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index ad4ad89d1..c1ce4b557 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -23,18 +23,18 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="DiscUtils.Udf" Version="0.16.4" />
+ <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
- <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.9" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-rc.2*" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.1" />
- <PackageReference Include="sharpcompress" Version="0.28.3" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
+ <PackageReference Include="sharpcompress" Version="0.30.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
- <PackageReference Include="DotNet.Glob" Version="3.1.2" />
+ <PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
@@ -42,16 +42,11 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
- <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
- </PropertyGroup>
-
- <PropertyGroup Condition=" '$(Configuration)' == 'Release'">
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index df48346e3..331de45c1 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Translates the physical item to user library.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of item.</typeparam>
/// <param name="item">The item.</param>
/// <param name="user">The user.</param>
/// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7010a6fb0..5f25f6980 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Sends a message asynchronously.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -150,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
- } while (
- (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+ }
+ while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
&& receiveresult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index aa80bccd7..e9d069cd3 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -42,6 +42,25 @@ namespace Emby.Server.Implementations.IO
private bool _disposed = false;
/// <summary>
+ /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ public LibraryMonitor(
+ ILogger<LibraryMonitor> logger,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager configurationManager,
+ IFileSystem fileSystem)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _configurationManager = configurationManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <summary>
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
/// </summary>
/// <param name="path">The path.</param>
@@ -95,21 +114,6 @@ namespace Emby.Server.Implementations.IO
}
}
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
- /// </summary>
- public LibraryMonitor(
- ILogger<LibraryMonitor> logger,
- ILibraryManager libraryManager,
- IServerConfigurationManager configurationManager,
- IFileSystem fileSystem)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _configurationManager = configurationManager;
- _fileSystem = fileSystem;
- }
-
private bool IsLibraryMonitorEnabled(BaseItem item)
{
if (item is BasePluginFolder)
@@ -199,7 +203,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="lst">The LST.</param>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">path</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
{
if (string.IsNullOrEmpty(path))
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 1bc229b0c..eeee28842 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
- protected ILogger<ManagedFileSystem> Logger;
+ private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.IO
ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths)
{
- Logger = logger;
+ _logger = logger;
_tempPath = applicationPaths.TempDirectory;
}
@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">filename</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual bool IsShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">filename</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -233,9 +233,9 @@ namespace Emby.Server.Implementations.IO
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
- //{
+ // {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
- //}
+ // }
if (info is FileInfo fileInfo)
{
@@ -246,15 +246,15 @@ namespace Emby.Server.Implementations.IO
{
try
{
- using (Stream thisFileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1))
+ using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
- result.Length = thisFileStream.Length;
+ result.Length = RandomAccess.GetLength(fileHandle);
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
- Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
+ _logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}
@@ -343,7 +343,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
+ _logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
@@ -382,7 +382,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
+ _logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index 1d97882db..3769ae4dd 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Server.Implementations
{
+ /// <summary>
+ /// Specifies the contract for server startup options.
+ /// </summary>
public interface IStartupOptions
{
/// <summary>
@@ -10,7 +11,7 @@ namespace Emby.Server.Implementations
string? FFmpegPath { get; }
/// <summary>
- /// Gets a value value indicating whether to run as service by the --service command line option.
+ /// Gets a value indicating whether to run as service by the --service command line option.
/// </summary>
bool IsService { get; }
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 4a026fd21..758986945 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -65,13 +65,13 @@ namespace Emby.Server.Implementations.Images
if (SupportedImages.Contains(ImageType.Primary))
{
var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false);
- updateType = updateType | primaryResult;
+ updateType |= primaryResult;
}
if (SupportedImages.Contains(ImageType.Thumb))
{
var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false);
- updateType = updateType | thumbResult;
+ updateType |= thumbResult;
}
return updateType;
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
new file mode 100644
index 000000000..1c69056d2
--- /dev/null
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -0,0 +1,67 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Images
+{
+ public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
+ where T : Folder, new()
+ {
+ private readonly ILibraryManager _libraryManager;
+
+ public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
+ {
+ return _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Parent = item,
+ DtoOptions = new DtoOptions(true),
+ ImageTypes = new ImageType[] { ImageType.Primary },
+ OrderBy = new (string, SortOrder)[]
+ {
+ (ItemSortBy.IsFolder, SortOrder.Ascending),
+ (ItemSortBy.SortName, SortOrder.Ascending)
+ },
+ Limit = 1
+ });
+ }
+
+ protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
+ {
+ return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
+ }
+
+ protected override bool Supports(BaseItem item)
+ {
+ return item is T;
+ }
+
+ protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
+ {
+ if (item is MusicAlbum)
+ {
+ return false;
+ }
+
+ return base.HasChangedByDate(item, image);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 859017f86..4376bd356 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -2,69 +2,16 @@
#pragma warning disable CS1591
-using System.Collections.Generic;
-using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
- public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
- where T : Folder, new()
- {
- protected ILibraryManager _libraryManager;
-
- public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor)
- {
- _libraryManager = libraryManager;
- }
-
- protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
- {
- return _libraryManager.GetItemList(new InternalItemsQuery
- {
- Parent = item,
- DtoOptions = new DtoOptions(true),
- ImageTypes = new ImageType[] { ImageType.Primary },
- OrderBy = new System.ValueTuple<string, SortOrder>[]
- {
- new System.ValueTuple<string, SortOrder>(ItemSortBy.IsFolder, SortOrder.Ascending),
- new System.ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
- },
- Limit = 1
- });
- }
-
- protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
- {
- return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
- }
-
- protected override bool Supports(BaseItem item)
- {
- return item is T;
- }
-
- protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
- {
- if (item is MusicAlbum)
- {
- return false;
- }
-
- return base.HasChangedByDate(item, image);
- }
- }
-
public class FolderImageProvider : BaseFolderImageProvider<Folder>
{
public FolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
@@ -87,20 +34,4 @@ namespace Emby.Server.Implementations.Images
return true;
}
}
-
- public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum>
- {
- public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
- {
- }
- }
-
- public class PhotoAlbumImageProvider : BaseFolderImageProvider<PhotoAlbum>
- {
- public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
- {
- }
- }
}
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 6da431c68..1f5090f7f 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -20,46 +19,6 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
/// <summary>
- /// Class MusicGenreImageProvider.
- /// </summary>
- public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
- {
- _libraryManager = libraryManager;
- }
-
- /// <summary>
- /// Get children objects used to create an music genre image.
- /// </summary>
- /// <param name="item">The music genre used to create the image.</param>
- /// <returns>Any relevant children objects.</returns>
- protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
- {
- return _libraryManager.GetItemList(new InternalItemsQuery
- {
- Genres = new[] { item.Name },
- IncludeItemTypes = new[]
- {
- nameof(MusicAlbum),
- nameof(MusicVideo),
- nameof(Audio)
- },
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
- Limit = 4,
- Recursive = true,
- ImageTypes = new[] { ImageType.Primary },
- DtoOptions = new DtoOptions(false)
- });
- }
- }
-
- /// <summary>
/// Class GenreImageProvider.
/// </summary>
public class GenreImageProvider : BaseDynamicImageProvider<Genre>
diff --git a/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs b/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
new file mode 100644
index 000000000..ce8367363
--- /dev/null
+++ b/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
@@ -0,0 +1,19 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Images
+{
+ public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum>
+ {
+ public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
new file mode 100644
index 000000000..baf1c9051
--- /dev/null
+++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
@@ -0,0 +1,59 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Images
+{
+ /// <summary>
+ /// Class MusicGenreImageProvider.
+ /// </summary>
+ public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
+ {
+ /// <summary>
+ /// The library manager.
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Get children objects used to create an music genre image.
+ /// </summary>
+ /// <param name="item">The music genre used to create the image.</param>
+ /// <returns>Any relevant children objects.</returns>
+ protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
+ {
+ return _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Genres = new[] { item.Name },
+ IncludeItemTypes = new[]
+ {
+ nameof(MusicAlbum),
+ nameof(MusicVideo),
+ nameof(Audio)
+ },
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
+ Limit = 4,
+ Recursive = true,
+ ImageTypes = new[] { ImageType.Primary },
+ DtoOptions = new DtoOptions(false)
+ });
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs b/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs
new file mode 100644
index 000000000..1ddb4c757
--- /dev/null
+++ b/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs
@@ -0,0 +1,19 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Images
+{
+ public class PhotoAlbumImageProvider : BaseFolderImageProvider<PhotoAlbum>
+ {
+ public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index c7d113963..bc5b4499f 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Ignore trailer folders but allow it at the collection level
- if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)
+ if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)
&& !(parent is AggregateFolder)
&& !(parent is UserRootFolder))
{
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
- if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
+ if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 6c65b5899..868071a99 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -4,6 +4,7 @@
using System;
using System.Globalization;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
@@ -41,6 +42,11 @@ namespace Emby.Server.Implementations.Library
return _closeFn();
}
+ public Stream GetStream()
+ {
+ throw new NotSupportedException();
+ }
+
public Task Open(CancellationToken openCancellationToken)
{
return Task.CompletedTask;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 8054beae3..1326f60fe 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -647,7 +647,7 @@ namespace Emby.Server.Implementations.Library
/// Determines whether a path should be ignored based on its contents - called after the contents have been read.
/// </summary>
/// <param name="args">The args.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
private static bool ShouldResolvePathContents(ItemResolveArgs args)
{
// Ignore any folders containing a file called .ignore
@@ -1250,10 +1250,8 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path)
{
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
- foreach (var file in files)
+ foreach (ReadOnlySpan<char> 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;
@@ -1268,7 +1266,7 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
public BaseItem GetItemById(Guid id)
{
if (id == Guid.Empty)
@@ -2714,7 +2712,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase))
+ .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
@@ -2758,7 +2756,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => BaseItem.AllExtrasTypesFolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 16b45161f..83acd8e9f 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -10,9 +10,9 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 6f83973ba..351fced34 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -13,9 +13,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -587,13 +587,6 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
- public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
- {
- var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
-
- return Task.FromResult(info.Value as IDirectStreamProvider);
- }
-
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
{
var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
@@ -602,7 +595,8 @@ namespace Emby.Server.Implementations.Library
public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
{
- var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
+ // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
+ var liveStreamInfo = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
var mediaSource = liveStreamInfo.MediaSource;
@@ -771,18 +765,19 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate(true);
}
- public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
+ public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
- var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
- return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
+ // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
+ var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
+ return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
}
- private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+ public ILiveStream GetLiveStreamInfo(string id)
{
if (string.IsNullOrEmpty(id))
{
@@ -791,12 +786,16 @@ namespace Emby.Server.Implementations.Library
if (_openStreams.TryGetValue(id, out ILiveStream info))
{
- return Task.FromResult(info);
- }
- else
- {
- return Task.FromException<ILiveStream>(new ResourceNotFoundException());
+ return info;
}
+
+ return null;
+ }
+
+ /// <inheritdoc />
+ public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
+ {
+ return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparison.OrdinalIgnoreCase));
}
public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 86b8039fa..d5b855cdf 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library
/// <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>
+ /// <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(
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 8e1eccb10..60720dd2f 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -82,6 +82,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary>
/// Determine if the supplied file data points to a music album.
/// </summary>
+ /// <param name="path">The path to check.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
public bool IsMusicAlbum(string path, IDirectoryService directoryService)
{
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index b102b86cf..9ff99fa43 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -275,6 +275,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Determines whether [is DVD directory] [the specified directory name].
/// </summary>
+ /// <param name="fullPath">The full path of the directory.</param>
+ /// <param name="directoryName">The name of the directory.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns><c>true</c> if the provided directory is a DVD directory, <c>false</c> otherwise.</returns>
protected bool IsDvdDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
{
if (!string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase))
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 68076730b..e685c87f1 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -49,13 +49,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
- var fileExtension = Path.GetExtension(f.FullName) ??
- string.Empty;
+ var fileExtension = Path.GetExtension(f.FullName)
+ ?? string.Empty;
return _validExtensions.Contains(
fileExtension,
- StringComparer
- .OrdinalIgnoreCase);
+ StringComparer.OrdinalIgnoreCase);
}).ToList();
// Don't return a Book if there is more (or less) than one document in the directory
diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
index 7aaee017d..db7703cd6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Class FolderResolver.
/// </summary>
- public class FolderResolver : FolderResolver<Folder>
+ public class FolderResolver : GenericFolderResolver<Folder>
{
/// <summary>
/// Gets the priority.
@@ -32,24 +32,4 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
}
-
- /// <summary>
- /// Class FolderResolver.
- /// </summary>
- /// <typeparam name="TItemType">The type of the T item type.</typeparam>
- public abstract class FolderResolver<TItemType> : ItemResolver<TItemType>
- where TItemType : Folder, new()
- {
- /// <summary>
- /// Sets the initial item values.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="args">The args.</param>
- protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
- {
- base.SetInitialItemValues(item, args);
-
- item.IsRoot = args.Parent == null;
- }
- }
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
new file mode 100644
index 000000000..f109a5e9a
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
@@ -0,0 +1,27 @@
+#nullable disable
+
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Class FolderResolver.
+ /// </summary>
+ /// <typeparam name="TItemType">The type of the T item type.</typeparam>
+ public abstract class GenericFolderResolver<TItemType> : ItemResolver<TItemType>
+ where TItemType : Folder, new()
+ {
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ item.IsRoot = args.Parent == null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 69d71d0d9..e7abe1e6d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <summary>
/// Class BoxSetResolver.
/// </summary>
- public class BoxSetResolver : FolderResolver<BoxSet>
+ public class BoxSetResolver : GenericFolderResolver<BoxSet>
{
/// <summary>
/// Resolves the specified args.
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 8b55a7744..f3b6ef0a2 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -24,6 +24,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// </summary>
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{
+ private readonly IImageProcessor _imageProcessor;
+
private string[] _validCollectionTypes = new[]
{
CollectionType.Movies,
@@ -33,8 +35,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
CollectionType.Photos
};
- private readonly IImageProcessor _imageProcessor;
-
/// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
/// </summary>
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
index 534bc80dd..1c560e8a6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Class PhotoAlbumResolver.
/// </summary>
- public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
+ public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
{
private readonly IImageProcessor _imageProcessor;
private readonly ILibraryManager _libraryManager;
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 2c4ead719..8ce59717d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items.
/// </summary>
- public class PlaylistResolver : FolderResolver<Playlist>
+ public class PlaylistResolver : GenericFolderResolver<Playlist>
{
private string[] _musicPlaylistCollectionTypes =
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
index 7b4e14334..6bb999641 100644
--- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -13,7 +13,7 @@ using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Resolvers
{
- public class SpecialFolderResolver : FolderResolver<Folder>
+ public class SpecialFolderResolver : GenericFolderResolver<Folder>
{
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
@@ -67,7 +67,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
return args.FileSystemChildren
.Where(i =>
{
-
try
{
return !i.IsDirectory &&
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index d6ae91056..cf2d22f4d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
- && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
+ && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
{
var episode = ResolveVideo<Episode>(args, false);
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 7d707df18..063f67543 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Class SeasonResolver.
/// </summary>
- public class SeasonResolver : FolderResolver<Season>
+ public class SeasonResolver : GenericFolderResolver<Season>
{
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index 4d8a6494c..b7fbe01c5 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Class SeriesResolver.
/// </summary>
- public class SeriesResolver : FolderResolver<Series>
+ public class SeriesResolver : GenericFolderResolver<Series>
{
private readonly ILogger<SeriesResolver> _logger;
private readonly ILibraryManager _libraryManager;
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 9d0a24a88..bebc25b77 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="query">The query.</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable{SearchHintResult}.</returns>
- /// <exception cref="ArgumentNullException">searchTerm</exception>
+ /// <exception cref="ArgumentException"><c>query.SearchTerm</c> is <c>null</c> or empty.</exception>
private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user)
{
var searchTerm = query.SearchTerm;
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index c4e230f21..bb3034142 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -25,8 +25,6 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class UserDataManager : IUserDataManager
{
- public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
-
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
@@ -44,6 +42,8 @@ namespace Emby.Server.Implementations.Library
_repository = repository;
}
+ public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
+
public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
@@ -90,10 +90,9 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
/// </summary>
- /// <param name="userId"></param>
- /// <param name="userData"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userData">The user item data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
@@ -104,8 +103,8 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Retrieve all user data for the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <returns>A <see cref="List{UserItemData}"/> containing all of the user's item data.</returns>
public List<UserItemData> GetAllUserData(Guid userId)
{
var user = _userManager.GetUserById(userId);
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index e2da672a3..711647e7a 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -341,19 +341,26 @@ namespace Emby.Server.Implementations.Library
mediaTypes = mediaTypes.Distinct().ToList();
}
- var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[]
- {
- nameof(Person),
- nameof(Studio),
- nameof(Year),
- nameof(MusicGenre),
- nameof(Genre)
- } : Array.Empty<string>();
+ var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
+ ? new[]
+ {
+ nameof(Person),
+ nameof(Studio),
+ nameof(Year),
+ nameof(MusicGenre),
+ nameof(Genre)
+ }
+ : Array.Empty<string>();
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = includeItemTypes,
- OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+ OrderBy = new[]
+ {
+ (ItemSortBy.DateCreated, SortOrder.Descending),
+ (ItemSortBy.SortName, SortOrder.Descending),
+ (ItemSortBy.ProductionYear, SortOrder.Descending)
+ },
IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null,
ExcludeItemTypes = excludeItemTypes,
IsVirtualItem = false,
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index f9a3e2c64..40436d9c5 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -95,10 +95,13 @@ namespace Emby.Server.Implementations.Library.Validators
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- }, false);
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
}
progress.Report(100);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index c5a9a92ec..6937cc097 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -45,21 +46,27 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
+ using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
onStarted();
- _logger.LogInformation("Copying recording stream to file {0}", targetFile);
+ _logger.LogInformation("Copying recording to file {FilePath}", targetFile);
// The media source is infinite so we need to handle stopping ourselves
using var durationToken = new CancellationTokenSource(duration);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
-
- await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
+ var linkedCancellationToken = cancellationTokenSource.Token;
+
+ await using var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream());
+ await _streamHelper.CopyToAsync(
+ fileStream,
+ output,
+ IODefaults.CopyToBufferSize,
+ 1000,
+ linkedCancellationToken).ConfigureAwait(false);
}
- _logger.LogInformation("Recording completed to file {0}", targetFile);
+ _logger.LogInformation("Recording completed: {FilePath}", targetFile);
}
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
@@ -71,8 +78,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO);
+ await using var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
onStarted();
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 026b6bc0b..980b42729 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1848,14 +1848,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
Indent = true,
- Encoding = Encoding.UTF8,
- CloseOutput = false
+ Encoding = Encoding.UTF8
};
using (var writer = XmlWriter.Create(stream, settings))
@@ -1913,14 +1911,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
Indent = true,
- Encoding = Encoding.UTF8,
- CloseOutput = false
+ Encoding = Encoding.UTF8
};
var options = _config.GetNfoConfiguration();
@@ -1990,7 +1986,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteElementString(
"dateadded",
- DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
+ DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue)
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index d806a0295..835028b92 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
@@ -188,7 +188,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile,
- targetFile,
+ targetFile.Replace("\"", "\\\""), // Escape quotes in filename
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
@@ -205,9 +205,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
// var audioChannels = 2;
// var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
// if (audioStream != null)
- //{
+ // {
// audioChannels = audioStream.Channels ?? audioChannels;
- //}
+ // }
// return "-codec:a:0 aac -strict experimental -ab 320000";
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
index dfe3517b2..7705132da 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
@@ -13,6 +13,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <summary>
/// Records the specified media source.
/// </summary>
+ /// <param name="directStreamProvider">The direct stream provider, or <c>null</c>.</param>
+ /// <param name="mediaSource">The media source.</param>
+ /// <param name="targetFile">The target file.</param>
+ /// <param name="duration">The duration to record.</param>
+ /// <param name="onStarted">An action to perform when recording starts.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A <see cref="Task"/> that represents the recording operation.</returns>
Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index 4a031e475..46979bfc5 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -1,9 +1,8 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -18,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private T[] _items;
+ private T[]? _items;
public ItemDataProvider(
ILogger logger,
@@ -34,6 +33,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
protected Func<T, T, bool> EqualityComparer { get; }
+ [MemberNotNull(nameof(_items))]
private void EnsureLoaded()
{
if (_items != null)
@@ -49,6 +49,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var bytes = File.ReadAllBytes(_dataPath);
_items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions);
+ if (_items == null)
+ {
+ Logger.LogError("Error deserializing {Path}, data was null", _dataPath);
+ _items = Array.Empty<T>();
+ }
+
return;
}
catch (JsonException ex)
@@ -62,7 +68,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void SaveList()
{
- Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath)));
var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
File.WriteAllText(_dataPath, jsonString);
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 8125ed57d..1f963e4a2 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -9,14 +9,15 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
@@ -34,7 +35,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ILogger<SchedulesDirect> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
- private readonly IApplicationHost _appHost;
private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
@@ -44,12 +44,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
IHttpClientFactory httpClientFactory,
- IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
_cryptoProvider = cryptoProvider;
}
@@ -114,18 +112,29 @@ 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<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (dailySchedules == null)
+ {
+ return Array.Empty<ProgramInfo>();
+ }
+
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
programRequestOptions.Headers.TryAddWithoutValidation("token", token);
- var programsID = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
- programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
+ var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
+ programRequestOptions.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(programIds, _jsonOptions));
+ programRequestOptions.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
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<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (programDetails == null)
+ {
+ return Array.Empty<ProgramInfo>();
+ }
+
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
var programIdsWithImages = programDetails
@@ -142,6 +151,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
// schedule.ProgramId + " which says it has images? " +
// programDict[schedule.ProgramId].hasImageArtwork);
+ if (string.IsNullOrEmpty(schedule.ProgramId))
+ {
+ continue;
+ }
+
if (images != null)
{
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
@@ -149,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var programEntry = programDict[schedule.ProgramId];
- var allImages = images[imageIndex].Data ?? new List<ImageDataDto>();
+ var allImages = images[imageIndex].Data;
var imagesWithText = allImages.Where(i => string.Equals(i.Text, "yes", StringComparison.OrdinalIgnoreCase));
var imagesWithoutText = allImages.Where(i => string.Equals(i.Text, "no", StringComparison.OrdinalIgnoreCase));
@@ -217,7 +231,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details)
{
- var startAt = GetDate(programInfo.AirDateTime);
+ if (programInfo.AirDateTime == null)
+ {
+ return null;
+ }
+
+ var startAt = programInfo.AirDateTime.Value;
var endAt = startAt.AddSeconds(programInfo.Duration);
var audioType = ProgramAudio.Stereo;
@@ -225,21 +244,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings
string newID = programId + "T" + startAt.Ticks + "C" + channelId;
- if (programInfo.AudioProperties != null)
+ if (programInfo.AudioProperties.Count != 0)
{
- if (programInfo.AudioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
+ if (programInfo.AudioProperties.Contains("atmos", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Atmos;
}
- else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("dd", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("stereo", StringComparer.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Stereo;
}
@@ -355,9 +374,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- if (!string.IsNullOrWhiteSpace(details.OriginalAirDate))
+ if (details.OriginalAirDate != null)
{
- info.OriginalAirDate = DateTime.Parse(details.OriginalAirDate, CultureInfo.InvariantCulture);
+ info.OriginalAirDate = details.OriginalAirDate;
info.ProductionYear = info.OriginalAirDate.Value.Year;
}
@@ -384,18 +403,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return info;
}
- private static DateTime GetDate(string value)
- {
- var date = DateTime.ParseExact(value, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", CultureInfo.InvariantCulture);
-
- if (date.Kind != DateTimeKind.Utc)
- {
- date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
- }
-
- return date;
- }
-
private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect)
{
var match = images
@@ -449,14 +456,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return result;
}
- private async Task<List<ShowImagesDto>> GetImageForPrograms(
+ private async Task<IReadOnlyList<ShowImagesDto>> GetImageForPrograms(
ListingsProviderInfo info,
IReadOnlyList<string> programIds,
CancellationToken cancellationToken)
{
if (programIds.Count == 0)
{
- return new List<ShowImagesDto>();
+ return Array.Empty<ShowImagesDto>();
}
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
@@ -480,13 +487,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await JsonSerializer.DeserializeAsync<List<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting image info from schedules direct");
- return new List<ShowImagesDto>();
+ return Array.Empty<ShowImagesDto>();
}
}
@@ -509,7 +516,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync<List<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root != null)
{
@@ -520,7 +527,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
lineups.Add(new NameIdPair
{
Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name,
- Id = lineup.Uri[18..]
+ Id = lineup.Uri?[18..]
});
}
}
@@ -651,7 +658,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (string.Equals(root.Message, "OK", StringComparison.Ordinal))
+ if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
return root.Token;
@@ -708,12 +715,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = httpResponse.Content;
var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- return root.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase));
+ return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
}
catch (HttpRequestException ex)
{
// SchedulesDirect returns 400 if no lineups are configured.
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ if (ex.StatusCode is HttpStatusCode.BadRequest)
{
return false;
}
@@ -779,10 +786,15 @@ 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<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (root == null)
+ {
+ return new List<ChannelInfo>();
+ }
+
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.Map.Count);
_logger.LogInformation("Mapping Stations to Channel");
- var allStations = root.Stations ?? new List<StationDto>();
+ var allStations = root.Stations;
var map = root.Map;
var list = new List<ChannelInfo>(map.Count);
@@ -790,11 +802,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var channelNumber = GetChannelNumber(channel);
- var station = allStations.Find(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase))
- ?? new StationDto
- {
- StationId = channel.StationId
- };
+ var stationIndex = allStations.FindIndex(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase));
+ var station = stationIndex == -1
+ ? new StationDto { StationId = channel.StationId }
+ : allStations[stationIndex];
var channelInfo = new ChannelInfo
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
index b881b307c..95ac996e0 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,24 +11,24 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the city.
/// </summary>
[JsonPropertyName("city")]
- public string City { get; set; }
+ public string? City { get; set; }
/// <summary>
/// Gets or sets the state.
/// </summary>
[JsonPropertyName("state")]
- public string State { get; set; }
+ public string? State { get; set; }
/// <summary>
/// Gets or sets the postal code.
/// </summary>
[JsonPropertyName("postalCode")]
- public string Postalcode { get; set; }
+ public string? Postalcode { get; set; }
/// <summary>
/// Gets or sets the country.
/// </summary>
[JsonPropertyName("country")]
- public string Country { get; set; }
+ public string? Country { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
index 96b67d1eb..f6251b9ad 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the content.
/// </summary>
[JsonPropertyName("content")]
- public string Content { get; set; }
+ public string? Content { get; set; }
/// <summary>
/// Gets or sets the lang.
/// </summary>
[JsonPropertyName("lang")]
- public string Lang { get; set; }
+ public string? Lang { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
index dac6f5f3e..0b7a2c63a 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,36 +11,36 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the billing order.
/// </summary>
[JsonPropertyName("billingOrder")]
- public string BillingOrder { get; set; }
+ public string? BillingOrder { get; set; }
/// <summary>
/// Gets or sets the role.
/// </summary>
[JsonPropertyName("role")]
- public string Role { get; set; }
+ public string? Role { get; set; }
/// <summary>
/// Gets or sets the name id.
/// </summary>
[JsonPropertyName("nameId")]
- public string NameId { get; set; }
+ public string? NameId { get; set; }
/// <summary>
/// Gets or sets the person id.
/// </summary>
[JsonPropertyName("personId")]
- public string PersonId { get; set; }
+ public string? PersonId { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
[JsonPropertyName("name")]
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the character name.
/// </summary>
[JsonPropertyName("characterName")]
- public string CharacterName { get; set; }
+ public string? CharacterName { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
index 8c9c2c1fc..87c327ed8 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,18 +13,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the list of maps.
/// </summary>
[JsonPropertyName("map")]
- public List<MapDto> Map { get; set; }
+ public IReadOnlyList<MapDto> Map { get; set; } = Array.Empty<MapDto>();
/// <summary>
/// Gets or sets the list of stations.
/// </summary>
[JsonPropertyName("stations")]
- public List<StationDto> Stations { get; set; }
+ public IReadOnlyList<StationDto> Stations { get; set; } = Array.Empty<StationDto>();
/// <summary>
/// Gets or sets the metadata.
/// </summary>
[JsonPropertyName("metadata")]
- public MetadataDto Metadata { get; set; }
+ public MetadataDto? Metadata { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
index 135b5bb08..c19cd2e48 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the body.
/// </summary>
[JsonPropertyName("body")]
- public string Body { get; set; }
+ public string? Body { get; set; }
/// <summary>
/// Gets or sets the code.
/// </summary>
[JsonPropertyName("code")]
- public string Code { get; set; }
+ public string? Code { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
index 82d1001c8..f00c9accd 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,30 +11,30 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the billing order.
/// </summary>
[JsonPropertyName("billingOrder")]
- public string BillingOrder { get; set; }
+ public string? BillingOrder { get; set; }
/// <summary>
/// Gets or sets the role.
/// </summary>
[JsonPropertyName("role")]
- public string Role { get; set; }
+ public string? Role { get; set; }
/// <summary>
/// Gets or sets the name id.
/// </summary>
[JsonPropertyName("nameId")]
- public string NameId { get; set; }
+ public string? NameId { get; set; }
/// <summary>
/// Gets or sets the person id.
/// </summary>
[JsonPropertyName("personId")]
- public string PersonId { get; set; }
+ public string? PersonId { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
[JsonPropertyName("name")]
- public string Name { get; set; }
+ public string? Name { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
index 68876b068..1a371965c 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -11,29 +10,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
public class DayDto
{
/// <summary>
- /// Initializes a new instance of the <see cref="DayDto"/> class.
- /// </summary>
- public DayDto()
- {
- Programs = new List<ProgramDto>();
- }
-
- /// <summary>
/// Gets or sets the station id.
/// </summary>
[JsonPropertyName("stationID")]
- public string StationId { get; set; }
+ public string? StationId { get; set; }
/// <summary>
/// Gets or sets the list of programs.
/// </summary>
[JsonPropertyName("programs")]
- public List<ProgramDto> Programs { get; set; }
+ public IReadOnlyList<ProgramDto> Programs { get; set; } = Array.Empty<ProgramDto>();
/// <summary>
/// Gets or sets the metadata schedule.
/// </summary>
[JsonPropertyName("metadata")]
- public MetadataScheduleDto Metadata { get; set; }
+ public MetadataScheduleDto? Metadata { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
index d3e6ff393..ca6ae7fb1 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the description language.
/// </summary>
[JsonPropertyName("descriptionLanguage")]
- public string DescriptionLanguage { get; set; }
+ public string? DescriptionLanguage { get; set; }
/// <summary>
/// Gets or sets the description.
/// </summary>
[JsonPropertyName("description")]
- public string Description { get; set; }
+ public string? Description { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
index 04360266c..1577219ed 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the description language.
/// </summary>
[JsonPropertyName("descriptionLanguage")]
- public string DescriptionLanguage { get; set; }
+ public string? DescriptionLanguage { get; set; }
/// <summary>
/// Gets or sets the description.
/// </summary>
[JsonPropertyName("description")]
- public string Description { get; set; }
+ public string? Description { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
index 3af36ae96..eaf4a340b 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,12 +13,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the list of description 100.
/// </summary>
[JsonPropertyName("description100")]
- public List<Description100Dto> Description100 { get; set; }
+ public IReadOnlyList<Description100Dto> Description100 { get; set; } = Array.Empty<Description100Dto>();
/// <summary>
/// Gets or sets the list of description1000.
/// </summary>
[JsonPropertyName("description1000")]
- public List<Description1000Dto> Description1000 { get; set; }
+ public IReadOnlyList<Description1000Dto> Description1000 { get; set; } = Array.Empty<Description1000Dto>();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
index c3b2bd9c1..fbdfb1f71 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,6 +11,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the sub type.
/// </summary>
[JsonPropertyName("subType")]
- public string SubType { get; set; }
+ public string? SubType { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
index 3d8bea362..6852d89d7 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
index 1fb3decb2..b9844562f 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,24 +13,24 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the headend.
/// </summary>
[JsonPropertyName("headend")]
- public string Headend { get; set; }
+ public string? Headend { get; set; }
/// <summary>
/// Gets or sets the transport.
/// </summary>
[JsonPropertyName("transport")]
- public string Transport { get; set; }
+ public string? Transport { get; set; }
/// <summary>
/// Gets or sets the location.
/// </summary>
[JsonPropertyName("location")]
- public string Location { get; set; }
+ public string? Location { get; set; }
/// <summary>
/// Gets or sets the list of lineups.
/// </summary>
[JsonPropertyName("lineups")]
- public List<LineupDto> Lineups { get; set; }
+ public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
index 912e680dd..a1ae3ca6d 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,60 +11,60 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the width.
/// </summary>
[JsonPropertyName("width")]
- public string Width { get; set; }
+ public string? Width { get; set; }
/// <summary>
/// Gets or sets the height.
/// </summary>
[JsonPropertyName("height")]
- public string Height { get; set; }
+ public string? Height { get; set; }
/// <summary>
/// Gets or sets the uri.
/// </summary>
[JsonPropertyName("uri")]
- public string Uri { get; set; }
+ public string? Uri { get; set; }
/// <summary>
/// Gets or sets the size.
/// </summary>
[JsonPropertyName("size")]
- public string Size { get; set; }
+ public string? Size { get; set; }
/// <summary>
/// Gets or sets the aspect.
/// </summary>
[JsonPropertyName("aspect")]
- public string aspect { get; set; }
+ public string? Aspect { get; set; }
/// <summary>
/// Gets or sets the category.
/// </summary>
[JsonPropertyName("category")]
- public string Category { get; set; }
+ public string? Category { get; set; }
/// <summary>
/// Gets or sets the text.
/// </summary>
[JsonPropertyName("text")]
- public string Text { get; set; }
+ public string? Text { get; set; }
/// <summary>
/// Gets or sets the primary.
/// </summary>
[JsonPropertyName("primary")]
- public string Primary { get; set; }
+ public string? Primary { get; set; }
/// <summary>
/// Gets or sets the tier.
/// </summary>
[JsonPropertyName("tier")]
- public string Tier { get; set; }
+ public string? Tier { get; set; }
/// <summary>
/// Gets or sets the caption.
/// </summary>
[JsonPropertyName("caption")]
- public CaptionDto Caption { get; set; }
+ public CaptionDto? Caption { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
index 52e920aa6..3dc64e5d8 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,30 +11,36 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the linup.
/// </summary>
[JsonPropertyName("lineup")]
- public string Lineup { get; set; }
+ public string? Lineup { get; set; }
/// <summary>
/// Gets or sets the lineup name.
/// </summary>
[JsonPropertyName("name")]
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the transport.
/// </summary>
[JsonPropertyName("transport")]
- public string Transport { get; set; }
+ public string? Transport { get; set; }
/// <summary>
/// Gets or sets the location.
/// </summary>
[JsonPropertyName("location")]
- public string Location { get; set; }
+ public string? Location { get; set; }
/// <summary>
/// Gets or sets the uri.
/// </summary>
[JsonPropertyName("uri")]
- public string Uri { get; set; }
+ public string? Uri { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this lineup was deleted.
+ /// </summary>
+ [JsonPropertyName("isDeleted")]
+ public bool? IsDeleted { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
index 15139ba3b..f19081781 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -20,18 +19,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the server id.
/// </summary>
[JsonPropertyName("serverID")]
- public string ServerId { get; set; }
+ public string? ServerId { get; set; }
/// <summary>
/// Gets or sets the datetime.
/// </summary>
[JsonPropertyName("datetime")]
- public string Datetime { get; set; }
+ public DateTime? LineupTimestamp { get; set; }
/// <summary>
/// Gets or sets the list of lineups.
/// </summary>
[JsonPropertyName("lineups")]
- public List<LineupDto> Lineups { get; set; }
+ public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
index 7b235ed7f..fecc55e03 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,7 +11,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the url.
/// </summary>
[JsonPropertyName("URL")]
- public string Url { get; set; }
+ public string? Url { get; set; }
/// <summary>
/// Gets or sets the height.
@@ -31,6 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the md5.
/// </summary>
[JsonPropertyName("md5")]
- public string Md5 { get; set; }
+ public string? Md5 { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
index 5140277b2..ffd02d474 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,19 +11,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the station id.
/// </summary>
[JsonPropertyName("stationID")]
- public string StationId { get; set; }
+ public string? StationId { get; set; }
/// <summary>
/// Gets or sets the channel.
/// </summary>
[JsonPropertyName("channel")]
- public string Channel { get; set; }
+ public string? Channel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the provider callsign.
+ /// </summary>
+ [JsonPropertyName("providerCallsign")]
+ public string? ProvderCallsign { get; set; }
/// <summary>
/// Gets or sets the logical channel number.
/// </summary>
[JsonPropertyName("logicalChannelNumber")]
- public string LogicalChannelNumber { get; set; }
+ public string? LogicalChannelNumber { get; set; }
/// <summary>
/// Gets or sets the uhfvhf.
@@ -44,5 +48,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// </summary>
[JsonPropertyName("atscMinor")]
public int AtscMinor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the match type.
+ /// </summary>
+ [JsonPropertyName("matchType")]
+ public string? MatchType { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
index 5a3893a35..40faa493c 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,18 +11,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the linup.
/// </summary>
[JsonPropertyName("lineup")]
- public string Lineup { get; set; }
+ public string? Lineup { get; set; }
/// <summary>
/// Gets or sets the modified timestamp.
/// </summary>
[JsonPropertyName("modified")]
- public string Modified { get; set; }
+ public string? Modified { get; set; }
/// <summary>
/// Gets or sets the transport.
/// </summary>
[JsonPropertyName("transport")]
- public string Transport { get; set; }
+ public string? Transport { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
index 4057e9802..43f290156 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -12,7 +10,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// <summary>
/// Gets or sets the gracenote object.
/// </summary>
- [JsonPropertyName("gracenote")]
- public GracenoteDto Gracenote { get; set; }
+ [JsonPropertyName("Gracenote")]
+ public GracenoteDto? Gracenote { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
index 4979296da..04560ab55 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,25 +12,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the modified timestamp.
/// </summary>
[JsonPropertyName("modified")]
- public string Modified { get; set; }
+ public string? Modified { get; set; }
/// <summary>
/// Gets or sets the md5.
/// </summary>
[JsonPropertyName("md5")]
- public string Md5 { get; set; }
+ public string? Md5 { get; set; }
/// <summary>
/// Gets or sets the start date.
/// </summary>
[JsonPropertyName("startDate")]
- public string StartDate { get; set; }
+ public DateTime? StartDate { get; set; }
/// <summary>
/// Gets or sets the end date.
/// </summary>
[JsonPropertyName("endDate")]
- public string EndDate { get; set; }
+ public DateTime? EndDate { get; set; }
/// <summary>
/// Gets or sets the days count.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
index 48d731d89..31bef423b 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the year.
/// </summary>
[JsonPropertyName("year")]
- public string Year { get; set; }
+ public string? Year { get; set; }
/// <summary>
/// Gets or sets the duration.
@@ -26,6 +25,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the list of quality rating.
/// </summary>
[JsonPropertyName("qualityRating")]
- public List<QualityRatingDto> QualityRating { get; set; }
+ public IReadOnlyList<QualityRatingDto> QualityRating { get; set; } = Array.Empty<QualityRatingDto>();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
index 42eddfff2..e8b15dc07 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
index a84c47c12..84c48f67f 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,85 +13,85 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the audience.
/// </summary>
[JsonPropertyName("audience")]
- public string Audience { get; set; }
+ public string? Audience { get; set; }
/// <summary>
/// Gets or sets the program id.
/// </summary>
[JsonPropertyName("programID")]
- public string ProgramId { get; set; }
+ public string? ProgramId { get; set; }
/// <summary>
/// Gets or sets the list of titles.
/// </summary>
[JsonPropertyName("titles")]
- public List<TitleDto> Titles { get; set; }
+ public IReadOnlyList<TitleDto> Titles { get; set; } = Array.Empty<TitleDto>();
/// <summary>
/// Gets or sets the event details object.
/// </summary>
[JsonPropertyName("eventDetails")]
- public EventDetailsDto EventDetails { get; set; }
+ public EventDetailsDto? EventDetails { get; set; }
/// <summary>
/// Gets or sets the descriptions.
/// </summary>
[JsonPropertyName("descriptions")]
- public DescriptionsProgramDto Descriptions { get; set; }
+ public DescriptionsProgramDto? Descriptions { get; set; }
/// <summary>
/// Gets or sets the original air date.
/// </summary>
[JsonPropertyName("originalAirDate")]
- public string OriginalAirDate { get; set; }
+ public DateTime? OriginalAirDate { get; set; }
/// <summary>
/// Gets or sets the list of genres.
/// </summary>
[JsonPropertyName("genres")]
- public List<string> Genres { get; set; }
+ public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the episode title.
/// </summary>
[JsonPropertyName("episodeTitle150")]
- public string EpisodeTitle150 { get; set; }
+ public string? EpisodeTitle150 { get; set; }
/// <summary>
/// Gets or sets the list of metadata.
/// </summary>
[JsonPropertyName("metadata")]
- public List<MetadataProgramsDto> Metadata { get; set; }
+ public IReadOnlyList<MetadataProgramsDto> Metadata { get; set; } = Array.Empty<MetadataProgramsDto>();
/// <summary>
/// Gets or sets the list of content raitings.
/// </summary>
[JsonPropertyName("contentRating")]
- public List<ContentRatingDto> ContentRating { get; set; }
+ public IReadOnlyList<ContentRatingDto> ContentRating { get; set; } = Array.Empty<ContentRatingDto>();
/// <summary>
/// Gets or sets the list of cast.
/// </summary>
[JsonPropertyName("cast")]
- public List<CastDto> Cast { get; set; }
+ public IReadOnlyList<CastDto> Cast { get; set; } = Array.Empty<CastDto>();
/// <summary>
/// Gets or sets the list of crew.
/// </summary>
[JsonPropertyName("crew")]
- public List<CrewDto> Crew { get; set; }
+ public IReadOnlyList<CrewDto> Crew { get; set; } = Array.Empty<CrewDto>();
/// <summary>
/// Gets or sets the entity type.
/// </summary>
[JsonPropertyName("entityType")]
- public string EntityType { get; set; }
+ public string? EntityType { get; set; }
/// <summary>
/// Gets or sets the show type.
/// </summary>
[JsonPropertyName("showType")]
- public string ShowType { get; set; }
+ public string? ShowType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether there is image artwork.
@@ -104,54 +103,54 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the primary image.
/// </summary>
[JsonPropertyName("primaryImage")]
- public string PrimaryImage { get; set; }
+ public string? PrimaryImage { get; set; }
/// <summary>
/// Gets or sets the thumb image.
/// </summary>
[JsonPropertyName("thumbImage")]
- public string ThumbImage { get; set; }
+ public string? ThumbImage { get; set; }
/// <summary>
/// Gets or sets the backdrop image.
/// </summary>
[JsonPropertyName("backdropImage")]
- public string BackdropImage { get; set; }
+ public string? BackdropImage { get; set; }
/// <summary>
/// Gets or sets the banner image.
/// </summary>
[JsonPropertyName("bannerImage")]
- public string BannerImage { get; set; }
+ public string? BannerImage { get; set; }
/// <summary>
/// Gets or sets the image id.
/// </summary>
[JsonPropertyName("imageID")]
- public string ImageId { get; set; }
+ public string? ImageId { get; set; }
/// <summary>
/// Gets or sets the md5.
/// </summary>
[JsonPropertyName("md5")]
- public string Md5 { get; set; }
+ public string? Md5 { get; set; }
/// <summary>
/// Gets or sets the list of content advisory.
/// </summary>
[JsonPropertyName("contentAdvisory")]
- public List<string> ContentAdvisory { get; set; }
+ public IReadOnlyList<string> ContentAdvisory { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the movie object.
/// </summary>
[JsonPropertyName("movie")]
- public MovieDto Movie { get; set; }
+ public MovieDto? Movie { get; set; }
/// <summary>
/// Gets or sets the list of recommendations.
/// </summary>
[JsonPropertyName("recommendations")]
- public List<RecommendationDto> Recommendations { get; set; }
+ public IReadOnlyList<RecommendationDto> Recommendations { get; set; } = Array.Empty<RecommendationDto>();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
index ad5389100..60389b45b 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,13 +13,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the program id.
/// </summary>
[JsonPropertyName("programID")]
- public string ProgramId { get; set; }
+ public string? ProgramId { get; set; }
/// <summary>
/// Gets or sets the air date time.
/// </summary>
[JsonPropertyName("airDateTime")]
- public string AirDateTime { get; set; }
+ public DateTime? AirDateTime { get; set; }
/// <summary>
/// Gets or sets the duration.
@@ -32,25 +31,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the md5.
/// </summary>
[JsonPropertyName("md5")]
- public string Md5 { get; set; }
+ public string? Md5 { get; set; }
/// <summary>
/// Gets or sets the list of audio properties.
/// </summary>
[JsonPropertyName("audioProperties")]
- public List<string> AudioProperties { get; set; }
+ public IReadOnlyList<string> AudioProperties { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the list of video properties.
/// </summary>
[JsonPropertyName("videoProperties")]
- public List<string> VideoProperties { get; set; }
+ public IReadOnlyList<string> VideoProperties { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the list of ratings.
/// </summary>
[JsonPropertyName("ratings")]
- public List<RatingDto> Ratings { get; set; }
+ public IReadOnlyList<RatingDto> Ratings { get; set; } = Array.Empty<RatingDto>();
/// <summary>
/// Gets or sets a value indicating whether this program is new.
@@ -62,13 +61,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the multipart object.
/// </summary>
[JsonPropertyName("multipart")]
- public MultipartDto Multipart { get; set; }
+ public MultipartDto? Multipart { get; set; }
/// <summary>
/// Gets or sets the live tape delay.
/// </summary>
[JsonPropertyName("liveTapeDelay")]
- public string LiveTapeDelay { get; set; }
+ public string? LiveTapeDelay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this is the premiere.
@@ -86,6 +85,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the premiere or finale.
/// </summary>
[JsonPropertyName("isPremiereOrFinale")]
- public string IsPremiereOrFinale { get; set; }
+ public string? IsPremiereOrFinale { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
index 5cd0a7459..c5ddcf7c5 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,30 +11,30 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the ratings body.
/// </summary>
[JsonPropertyName("ratingsBody")]
- public string RatingsBody { get; set; }
+ public string? RatingsBody { get; set; }
/// <summary>
/// Gets or sets the rating.
/// </summary>
[JsonPropertyName("rating")]
- public string Rating { get; set; }
+ public string? Rating { get; set; }
/// <summary>
/// Gets or sets the min rating.
/// </summary>
[JsonPropertyName("minRating")]
- public string MinRating { get; set; }
+ public string? MinRating { get; set; }
/// <summary>
/// Gets or sets the max rating.
/// </summary>
[JsonPropertyName("maxRating")]
- public string MaxRating { get; set; }
+ public string? MaxRating { get; set; }
/// <summary>
/// Gets or sets the increment.
/// </summary>
[JsonPropertyName("increment")]
- public string Increment { get; set; }
+ public string? Increment { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
index 948b83144..e04b619a4 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the body.
/// </summary>
[JsonPropertyName("body")]
- public string Body { get; set; }
+ public string? Body { get; set; }
/// <summary>
/// Gets or sets the code.
/// </summary>
[JsonPropertyName("code")]
- public string Code { get; set; }
+ public string? Code { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
index 1308f45ce..c8f79fd1c 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,12 +11,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the program id.
/// </summary>
[JsonPropertyName("programID")]
- public string ProgramId { get; set; }
+ public string? ProgramId { get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
[JsonPropertyName("title120")]
- public string Title120 { get; set; }
+ public string? Title120 { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
index fb7a31ac8..0cd05709b 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,12 +13,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the station id.
/// </summary>
[JsonPropertyName("stationID")]
- public string StationId { get; set; }
+ public string? StationId { get; set; }
/// <summary>
/// Gets or sets the list of dates.
/// </summary>
[JsonPropertyName("date")]
- public List<string> Date { get; set; }
+ public IReadOnlyList<string> Date { get; set; } = Array.Empty<string>();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
index 34302370d..84e224b71 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -14,12 +13,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the program id.
/// </summary>
[JsonPropertyName("programID")]
- public string ProgramId { get; set; }
+ public string? ProgramId { get; set; }
/// <summary>
/// Gets or sets the list of data.
/// </summary>
[JsonPropertyName("data")]
- public List<ImageDataDto> Data { get; set; }
+ public IReadOnlyList<ImageDataDto> Data { get; set; } = Array.Empty<ImageDataDto>();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
index 12f3576c6..d797fd49b 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
@@ -1,67 +1,66 @@
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
- /// Station dto.
- /// </summary>
- public class StationDto
- {
- /// <summary>
- /// Gets or sets the station id.
- /// </summary>
- [JsonPropertyName("stationID")]
- public string StationId { get; set; }
+ /// Station dto.
+ /// </summary>
+ public class StationDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- [JsonPropertyName("name")]
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
- /// <summary>
- /// Gets or sets the callsign.
- /// </summary>
- [JsonPropertyName("callsign")]
- public string Callsign { get; set; }
+ /// <summary>
+ /// Gets or sets the callsign.
+ /// </summary>
+ [JsonPropertyName("callsign")]
+ public string? Callsign { get; set; }
- /// <summary>
- /// Gets or sets the broadcast language.
- /// </summary>
- [JsonPropertyName("broadcastLanguage")]
- public List<string> BroadcastLanguage { get; set; }
+ /// <summary>
+ /// Gets or sets the broadcast language.
+ /// </summary>
+ [JsonPropertyName("broadcastLanguage")]
+ public IReadOnlyList<string> BroadcastLanguage { get; set; } = Array.Empty<string>();
- /// <summary>
- /// Gets or sets the description language.
- /// </summary>
- [JsonPropertyName("descriptionLanguage")]
- public List<string> DescriptionLanguage { get; set; }
+ /// <summary>
+ /// Gets or sets the description language.
+ /// </summary>
+ [JsonPropertyName("descriptionLanguage")]
+ public IReadOnlyList<string> DescriptionLanguage { get; set; } = Array.Empty<string>();
- /// <summary>
- /// Gets or sets the broadcaster.
- /// </summary>
- [JsonPropertyName("broadcaster")]
- public BroadcasterDto Broadcaster { get; set; }
+ /// <summary>
+ /// Gets or sets the broadcaster.
+ /// </summary>
+ [JsonPropertyName("broadcaster")]
+ public BroadcasterDto? Broadcaster { get; set; }
- /// <summary>
- /// Gets or sets the affiliate.
- /// </summary>
- [JsonPropertyName("affiliate")]
- public string Affiliate { get; set; }
+ /// <summary>
+ /// Gets or sets the affiliate.
+ /// </summary>
+ [JsonPropertyName("affiliate")]
+ public string? Affiliate { get; set; }
- /// <summary>
- /// Gets or sets the logo.
- /// </summary>
- [JsonPropertyName("logo")]
- public LogoDto Logo { get; set; }
+ /// <summary>
+ /// Gets or sets the logo.
+ /// </summary>
+ [JsonPropertyName("logo")]
+ public LogoDto? Logo { get; set; }
- /// <summary>
- /// Gets or set a value indicating whether it is commercial free.
- /// </summary>
- [JsonPropertyName("isCommercialFree")]
- public bool? IsCommercialFree { get; set; }
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether it is commercial free.
+ /// </summary>
+ [JsonPropertyName("isCommercialFree")]
+ public bool? IsCommercialFree { get; set; }
+ }
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
index 06c95524b..61cd4a9b0 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -13,6 +11,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the title.
/// </summary>
[JsonPropertyName("title120")]
- public string Title120 { get; set; }
+ public string? Title120 { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
index c3ec1c7d6..afb999486 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Text.Json.Serialization;
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
@@ -19,18 +18,30 @@ namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the response message.
/// </summary>
[JsonPropertyName("message")]
- public string Message { get; set; }
+ public string? Message { get; set; }
/// <summary>
/// Gets or sets the server id.
/// </summary>
[JsonPropertyName("serverID")]
- public string ServerId { get; set; }
+ public string? ServerId { get; set; }
/// <summary>
/// Gets or sets the token.
/// </summary>
[JsonPropertyName("token")]
- public string Token { get; set; }
+ public string? Token { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current datetime.
+ /// </summary>
+ [JsonPropertyName("datetime")]
+ public DateTime? TokenTimestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the response message.
+ /// </summary>
+ [JsonPropertyName("response")]
+ public string? Response { get; set; }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 1c103f402..0c0ec48d9 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using Jellyfin.XmlTv;
using Jellyfin.XmlTv.Entities;
using MediaBrowser.Common.Extensions;
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO))
+ await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous))
{
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
@@ -89,11 +90,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return UnzipIfNeeded(info.Path, cacheFile);
}
- private string UnzipIfNeeded(string originalUrl, string file)
+ private string UnzipIfNeeded(ReadOnlySpan<char> originalUrl, string file)
{
- string ext = Path.GetExtension(originalUrl.Split('?')[0]);
+ ReadOnlySpan<char> ext = Path.GetExtension(originalUrl.LeftPart('?'));
- if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 096b7f045..2b82f2462 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -23,10 +23,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
- protected readonly IServerConfigurationManager Config;
- protected readonly ILogger<BaseTunerHost> Logger;
- protected readonly IFileSystem FileSystem;
-
private readonly IMemoryCache _memoryCache;
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
@@ -37,12 +33,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
FileSystem = fileSystem;
}
- public virtual bool IsSupported => true;
+ protected IServerConfigurationManager Config { get; }
- protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+ protected ILogger<BaseTunerHost> Logger { get; }
+
+ protected IFileSystem FileSystem { get; }
+
+ public virtual bool IsSupported => true;
public abstract string Type { get; }
+ protected virtual string ChannelIdPrefix => Type + "_";
+
+ protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{
var key = tuner.Id;
@@ -217,8 +221,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
throw new LiveTvConflictException();
}
- protected virtual string ChannelIdPrefix => Type + "_";
-
protected virtual bool IsValidChannelId(string channelId)
{
if (string.IsNullOrEmpty(channelId))
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
new file mode 100644
index 000000000..069b4fab6
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
@@ -0,0 +1,35 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class HdHomerunChannelCommands : IHdHomerunChannelCommands
+ {
+ private string? _channel;
+ private string? _profile;
+
+ public HdHomerunChannelCommands(string? channel, string? profile)
+ {
+ _channel = channel;
+ _profile = profile;
+ }
+
+ public IEnumerable<(string, string)> GetCommands()
+ {
+ if (!string.IsNullOrEmpty(_channel))
+ {
+ if (!string.IsNullOrEmpty(_profile)
+ && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
+ {
+ yield return ("vchannel", $"{_channel} transcode={_profile}");
+ }
+ else
+ {
+ yield return ("vchannel", _channel);
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 2bd12a9c8..78ea7bd0f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -36,7 +36,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly ISocketFactory _socketFactory;
- private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
private readonly JsonSerializerOptions _jsonOptions;
@@ -50,7 +49,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
- INetworkManager networkManager,
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
@@ -58,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_socketFactory = socketFactory;
- _networkManager = networkManager;
_streamHelper = streamHelper;
_jsonOptions = JsonDefaults.Options;
@@ -70,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
protected override string ChannelIdPrefix => "hdhr_";
- private string GetChannelId(TunerHostInfo info, Channels i)
+ private string GetChannelId(Channels i)
=> ChannelIdPrefix + i.GuideNumber;
internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
@@ -90,11 +87,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return lineup.Where(i => !i.DRM).ToList();
}
- private class HdHomerunChannelInfo : ChannelInfo
- {
- public bool IsLegacyTuner { get; set; }
- }
-
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
{
var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false);
@@ -103,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
Name = i.GuideName,
Number = i.GuideNumber,
- Id = GetChannelId(tuner, i),
+ Id = GetChannelId(i),
IsFavorite = i.Favorite,
TunerHostId = tuner.Id,
IsHD = i.HD,
@@ -255,7 +247,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- var tuners = new List<LiveTvTunerInfo>();
+ var tuners = new List<LiveTvTunerInfo>(model.TunerCount);
var uri = new Uri(GetApiUrl(info));
@@ -264,10 +256,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Legacy HdHomeruns are IPv4 only
var ipInfo = IPAddress.Parse(uri.Host);
- for (int i = 0; i < model.TunerCount; ++i)
+ for (int i = 0; i < model.TunerCount; i++)
{
var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
- var currentChannel = "none"; // @todo Get current channel and map back to Station Id
+ var currentChannel = "none"; // TODO: Get current channel and map back to Station Id
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
tuners.Add(new LiveTvTunerInfo
@@ -455,28 +447,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Path = url,
Protocol = MediaProtocol.Udp,
MediaStreams = new List<MediaStream>
- {
- new MediaStream
- {
- Type = MediaStreamType.Video,
- // Set the index to -1 because we don't know the exact index of the video stream within the container
- Index = -1,
- IsInterlaced = isInterlaced,
- Codec = videoCodec,
- Width = width,
- Height = height,
- BitRate = videoBitrate,
- NalLengthSize = nal
- },
- new MediaStream
- {
- Type = MediaStreamType.Audio,
- // Set the index to -1 because we don't know the exact index of the audio stream within the container
- Index = -1,
- Codec = audioCodec,
- BitRate = audioBitrate
- }
- },
+ {
+ new MediaStream
+ {
+ Type = MediaStreamType.Video,
+ // Set the index to -1 because we don't know the exact index of the video stream within the container
+ Index = -1,
+ IsInterlaced = isInterlaced,
+ Codec = videoCodec,
+ Width = width,
+ Height = height,
+ BitRate = videoBitrate,
+ NalLengthSize = nal
+ },
+ new MediaStream
+ {
+ Type = MediaStreamType.Audio,
+ // Set the index to -1 because we don't know the exact index of the audio stream within the container
+ Index = -1,
+ Codec = audioCodec,
+ BitRate = audioBitrate
+ }
+ },
RequiresOpening = true,
RequiresClosing = true,
BufferMs = 0,
@@ -551,7 +543,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- var profile = streamId.Split('_')[0];
+ var profile = streamId.AsSpan().LeftPart('_').ToString();
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile);
@@ -718,5 +710,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return hostInfo;
}
+
+ private class HdHomerunChannelInfo : ChannelInfo
+ {
+ public bool IsLegacyTuner { get; set; }
+ }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index b2e555c7d..f9d151a81 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -5,12 +5,10 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
-using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Text;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
@@ -18,70 +16,6 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
- public interface IHdHomerunChannelCommands
- {
- IEnumerable<(string, string)> GetCommands();
- }
-
- public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
- {
- private string _channel;
- private string _program;
-
- public LegacyHdHomerunChannelCommands(string url)
- {
- // parse url for channel and program
- var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
- var match = regExp.Match(url);
- if (match.Success)
- {
- _channel = match.Groups[1].Value;
- _program = match.Groups[2].Value;
- }
- }
-
- public IEnumerable<(string, string)> GetCommands()
- {
- if (!string.IsNullOrEmpty(_channel))
- {
- yield return ("channel", _channel);
- }
-
- if (!string.IsNullOrEmpty(_program))
- {
- yield return ("program", _program);
- }
- }
- }
-
- public class HdHomerunChannelCommands : IHdHomerunChannelCommands
- {
- private string _channel;
- private string _profile;
-
- public HdHomerunChannelCommands(string channel, string profile)
- {
- _channel = channel;
- _profile = profile;
- }
-
- public IEnumerable<(string, string)> GetCommands()
- {
- if (!string.IsNullOrEmpty(_channel))
- {
- if (!string.IsNullOrEmpty(_profile)
- && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
- {
- yield return ("vchannel", $"{_channel} transcode={_profile}");
- }
- else
- {
- yield return ("vchannel", _channel);
- }
- }
- }
- }
-
public sealed class HdHomerunManager : IDisposable
{
public const int HdHomeRunPort = 65001;
@@ -150,8 +84,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
if (!_lockkey.HasValue)
{
- var rand = new Random();
- _lockkey = (uint)rand.Next();
+ _lockkey = (uint)Random.Shared.Next();
}
var lockKeyValue = _lockkey.Value;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 58e0c7448..31445e1ec 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -101,7 +101,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- if (localAddress.IsIPv4MappedToIPv6) {
+ if (localAddress.IsIPv4MappedToIPv6)
+ {
localAddress = localAddress.MapToIPv4();
}
@@ -156,11 +157,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false);
}
- public string GetFilePath()
- {
- return TempFilePath;
- }
-
private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
using (udpClient)
@@ -184,7 +180,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
EnableStreamSharing = false;
}
- await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
+ await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
}
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
@@ -201,7 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
cancellationToken,
timeOutSource.Token))
{
- var resTask = udpClient.ReceiveAsync();
+ var resTask = udpClient.ReceiveAsync(linkedSource.Token).AsTask();
if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask)
{
resTask.Dispose();
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
new file mode 100644
index 000000000..153354932
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
@@ -0,0 +1,11 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public interface IHdHomerunChannelCommands
+ {
+ IEnumerable<(string, string)> GetCommands();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
new file mode 100644
index 000000000..26627b8aa
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
@@ -0,0 +1,38 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
+ {
+ private string? _channel;
+ private string? _program;
+
+ public LegacyHdHomerunChannelCommands(string url)
+ {
+ // parse url for channel and program
+ var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
+ var match = regExp.Match(url);
+ if (match.Success)
+ {
+ _channel = match.Groups[1].Value;
+ _program = match.Groups[2].Value;
+ }
+ }
+
+ public IEnumerable<(string, string)> GetCommands()
+ {
+ if (!string.IsNullOrEmpty(_channel))
+ {
+ yield return ("channel", _channel);
+ }
+
+ if (!string.IsNullOrEmpty(_program))
+ {
+ yield return ("program", _program);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 2c21a4a89..5581ba87c 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -3,10 +3,8 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -22,14 +20,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
private readonly IConfigurationManager _configurationManager;
- protected readonly IFileSystem FileSystem;
-
- protected readonly IStreamHelper StreamHelper;
-
- protected string TempFilePath;
- protected readonly ILogger Logger;
- protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource();
-
public LiveStream(
MediaSourceInfo mediaSource,
TunerHostInfo tuner,
@@ -57,7 +47,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
SetTempFilePath("ts");
}
- protected virtual int EmptyReadLimit => 1000;
+ protected IFileSystem FileSystem { get; }
+
+ protected IStreamHelper StreamHelper { get; }
+
+ protected ILogger Logger { get; }
+
+ protected CancellationTokenSource LiveStreamCancellationTokenSource { get; } = new CancellationTokenSource();
+
+ protected string TempFilePath { get; set; }
public MediaSourceInfo OriginalMediaSource { get; set; }
@@ -97,121 +95,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.CompletedTask;
}
- protected FileStream GetInputStream(string path, bool allowAsyncFileRead)
+ public Stream GetStream()
+ {
+ var stream = GetInputStream(TempFilePath);
+ bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
+ if (seekFile)
+ {
+ TrySeek(stream, -20000);
+ }
+
+ return stream;
+ }
+
+ protected FileStream GetInputStream(string path)
=> new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
IODefaults.FileStreamBufferSize,
- allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
+ FileOptions.SequentialScan | FileOptions.Asynchronous);
- public Task DeleteTempFiles()
- {
- return DeleteTempFiles(GetStreamFilePaths());
- }
-
- protected async Task DeleteTempFiles(IEnumerable<string> paths, int retryCount = 0)
+ protected async Task DeleteTempFiles(string path, int retryCount = 0)
{
if (retryCount == 0)
{
- Logger.LogInformation("Deleting temp files {0}", paths);
- }
-
- var failedFiles = new List<string>();
-
- foreach (var path in paths)
- {
- if (!File.Exists(path))
- {
- continue;
- }
-
- try
- {
- FileSystem.DeleteFile(path);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error deleting file {path}", path);
- failedFiles.Add(path);
- }
- }
-
- if (failedFiles.Count > 0 && retryCount <= 40)
- {
- await Task.Delay(500).ConfigureAwait(false);
- await DeleteTempFiles(failedFiles, retryCount + 1).ConfigureAwait(false);
+ Logger.LogInformation("Deleting temp file {FilePath}", path);
}
- }
-
- protected virtual List<string> GetStreamFilePaths()
- {
- return new List<string> { TempFilePath };
- }
-
- public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
- {
- using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
- cancellationToken = linkedCancellationTokenSource.Token;
- bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
-
- var nextFileInfo = GetNextFile(null);
- var nextFile = nextFileInfo.file;
- var isLastFile = nextFileInfo.isLastFile;
-
- var allowAsync = AsyncFile.UseAsyncIO;
- while (!string.IsNullOrEmpty(nextFile))
- {
- var emptyReadLimit = isLastFile ? EmptyReadLimit : 1;
-
- await CopyFile(nextFile, seekFile, emptyReadLimit, allowAsync, stream, cancellationToken).ConfigureAwait(false);
-
- seekFile = false;
- nextFileInfo = GetNextFile(nextFile);
- nextFile = nextFileInfo.file;
- isLastFile = nextFileInfo.isLastFile;
- }
-
- Logger.LogInformation("Live Stream ended.");
- }
-
- private (string file, bool isLastFile) GetNextFile(string currentFile)
- {
- var files = GetStreamFilePaths();
-
- if (string.IsNullOrEmpty(currentFile))
+ try
{
- return (files[^1], true);
+ FileSystem.DeleteFile(path);
}
-
- var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
-
- var isLastFile = nextIndex == files.Count - 1;
-
- return (files.ElementAtOrDefault(nextIndex), isLastFile);
- }
-
- private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken)
- {
- using (var inputStream = GetInputStream(path, allowAsync))
+ catch (Exception ex)
{
- if (seekFile)
+ Logger.LogError(ex, "Error deleting file {FilePath}", path);
+ if (retryCount <= 40)
{
- TrySeek(inputStream, -20000);
+ await Task.Delay(500).ConfigureAwait(false);
+ await DeleteTempFiles(path, retryCount + 1).ConfigureAwait(false);
}
-
- await StreamHelper.CopyToAsync(
- inputStream,
- stream,
- IODefaults.CopyToBufferSize,
- emptyReadLimit,
- cancellationToken).ConfigureAwait(false);
}
}
- private void TrySeek(FileStream stream, long offset)
+ private void TrySeek(Stream stream, long offset)
{
if (!stream.CanSeek)
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 23071a430..506ef5548 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -238,7 +238,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
+ numberString = Path.GetFileNameWithoutExtension(mediaUrl.AsSpan().RightPart('/')).ToString();
if (!IsValidChannelNumber(numberString))
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 862993877..3b69e55b0 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
@@ -55,39 +54,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
var typeName = GetType().Name;
- Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
+ Logger.LogInformation("Opening {StreamType} Live stream from {Url}", typeName, url);
// Response stream is disposed manually.
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
- var extension = "ts";
- var requiresRemux = false;
-
var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
- if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
- {
- requiresRemux = true;
- }
- else if (contentType.IndexOf("mp4", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("dash", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("mpegURL", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("text/", StringComparison.OrdinalIgnoreCase) != -1)
- {
- requiresRemux = true;
- }
-
- // Close the stream without any sharing features
- if (requiresRemux)
+ if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
{
- using (response)
- {
- return;
- }
+ // Close the stream without any sharing features
+ response.Dispose();
+ return;
}
- SetTempFilePath(extension);
+ SetTempFilePath("ts");
var taskCompletionSource = new TaskCompletionSource<bool>();
@@ -117,16 +103,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!taskCompletionSource.Task.Result)
{
- Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
+ Logger.LogWarning("Zero bytes copied from stream {StreamType} to {FilePath} but no exception raised", GetType().Name, TempFilePath);
throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
}
}
- public string GetFilePath()
- {
- return TempFilePath;
- }
-
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(
@@ -134,10 +115,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
+ Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
using var message = response;
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await StreamHelper.CopyToAsync(
stream,
fileStream,
@@ -147,19 +128,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
catch (OperationCanceledException ex)
{
- Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
+ Logger.LogInformation("Copying of {StreamType} to {FilePath} was canceled", GetType().Name, TempFilePath);
openTaskCompletionSource.TrySetException(ex);
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
+ Logger.LogError(ex, "Error copying live stream {StreamType} to {FilePath}", GetType().Name, TempFilePath);
openTaskCompletionSource.TrySetException(ex);
}
openTaskCompletionSource.TrySetResult(false);
EnableStreamSharing = false;
- await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
+ await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
},
CancellationToken.None);
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index be629c8a4..ee70aa8d8 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -1,5 +1,5 @@
{
- "Albums": "البومات",
+ "Albums": "ألبومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق",
"Artists": "الفنانين",
@@ -8,14 +8,14 @@
"CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}",
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
- "Collections": "مجموعات",
+ "Collections": "التجميعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
"Favorites": "المفضلة",
"Folders": "المجلدات",
"Genres": "التضنيفات",
- "HeaderAlbumArtists": "فناني الألبومات",
+ "HeaderAlbumArtists": "ألبوم الفنان",
"HeaderContinueWatching": "استئناف",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "التلفاز المباشر",
"HeaderNextUp": "التالي",
"HeaderRecordingGroups": "مجموعات التسجيل",
- "HomeVideos": "الفيديوهات المنزلية",
+ "HomeVideos": "الفيديوهات الشخصية",
"Inherit": "توريث",
"ItemAddedWithName": "تم إضافة {0} للمكتبة",
"ItemRemovedWithName": "تم إزالة {0} من المكتبة",
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 7715daa7c..db3c13d80 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -15,7 +15,7 @@
"Favorites": "Preferits",
"Folders": "Carpetes",
"Genres": "Gèneres",
- "HeaderAlbumArtists": "Artistes del Àlbum",
+ "HeaderAlbumArtists": "Àlbum de l'artista",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Predilectes",
diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json
new file mode 100644
index 000000000..0fa72dea4
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/cy.json
@@ -0,0 +1,14 @@
+{
+ "DeviceOnlineWithName": "Mae {0} wedi'i gysylltu",
+ "DeviceOfflineWithName": "Mae {0} wedi datgysylltu",
+ "Default": "Diofyn",
+ "Collections": "Casgliadau",
+ "ChapterNameValue": "Pennod {0}",
+ "Channels": "Sianeli",
+ "CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
+ "Books": "Llyfrau",
+ "AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
+ "Artists": "Artistiaid",
+ "AppDeviceValues": "Ap: {0}, Dyfais: {1}",
+ "Albums": "Albwmau"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 8b2e8b6b1..86ce9240e 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -15,7 +15,7 @@
"Favorites": "Favourites",
"Folders": "Folders",
"Genres": "Genres",
- "HeaderAlbumArtists": "Album Artists",
+ "HeaderAlbumArtists": "Artist's Album",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
- "HomeVideos": "Home videos",
+ "HomeVideos": "Home Videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
@@ -39,7 +39,7 @@
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
- "MusicVideos": "Music videos",
+ "MusicVideos": "Music Videos",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index ca615cc8c..7ce87502c 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -1,17 +1,17 @@
{
"NotificationOptionInstallationFailed": "Instalada fiasko",
- "NotificationOptionAudioPlaybackStopped": "Sono de ludado haltis",
- "NotificationOptionAudioPlayback": "Ludado de sono startis",
+ "NotificationOptionAudioPlaybackStopped": "Ludado de sono haltis",
+ "NotificationOptionAudioPlayback": "Ludado de sono lanĉis",
"NameSeasonUnknown": "Sezono Nekonata",
"NameSeasonNumber": "Sezono {0}",
"NameInstallFailed": "{0} instalado fiaskis",
"Music": "Muziko",
"Movies": "Filmoj",
- "ItemRemovedWithName": "{0} forigis el la biblioteko",
- "ItemAddedWithName": "{0} aldonis al la biblioteko",
- "HeaderLiveTV": "Viva Televido",
- "HeaderContinueWatching": "Daŭrigi Spektado",
- "HeaderAlbumArtists": "Artistoj de Albumo",
+ "ItemRemovedWithName": "{0} forigis el la libraro",
+ "ItemAddedWithName": "{0} aldonis al la libraro",
+ "HeaderLiveTV": "TV-etero",
+ "HeaderContinueWatching": "Daŭrigi Spektadon",
+ "HeaderAlbumArtists": "Albumo de artisto",
"Folders": "Dosierujoj",
"DeviceOnlineWithName": "{0} estas konektita",
"Default": "Defaŭlte",
@@ -30,7 +30,7 @@
"User": "Uzanto",
"System": "Sistemo",
"Songs": "Kantoj",
- "ScheduledTaskStartedWithName": "{0} komencis",
+ "ScheduledTaskStartedWithName": "{0} lanĉis",
"ScheduledTaskFailedWithName": "{0} malsukcesis",
"PluginUninstalledWithName": "{0} malinstaliĝis",
"PluginInstalledWithName": "{0} instaliĝis",
@@ -43,5 +43,81 @@
"MusicVideos": "Muzikvideoj",
"LabelIpAddressValue": "IP-adreso: {0}",
"Genres": "Ĝenroj",
- "DeviceOfflineWithName": "{0} malkonektis"
+ "DeviceOfflineWithName": "{0} malkonektis",
+ "HeaderFavoriteArtists": "Favorataj Artistoj",
+ "Shows": "Serioj",
+ "HeaderFavoriteShows": "Favorataj Serioj",
+ "TvShows": "TV-serioj",
+ "Favorites": "Favoratoj",
+ "TaskCleanLogs": "Purigi Ĵurnalan Katalogon",
+ "TaskRefreshLibrary": "Skanu Plurmedian Libraron",
+ "ValueSpecialEpisodeName": "Speciala - {0}",
+ "TaskOptimizeDatabase": "Optimigi datumbazon",
+ "TaskRefreshChannels": "Refreŝigi Kanalojn",
+ "TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn",
+ "TaskRefreshPeople": "Refreŝigi Homojn",
+ "TasksChannelsCategory": "Interretaj Kanaloj",
+ "ProviderValue": "Provizanto: {0}",
+ "NotificationOptionPluginError": "Kromprograma malsukceso",
+ "MixedContent": "Miksita enhavo",
+ "TasksApplicationCategory": "Aplikaĵo",
+ "TasksMaintenanceCategory": "Prizorgado",
+ "Undefined": "Nedifinita",
+ "Sync": "Sinkronigo",
+ "Latest": "Plej novaj",
+ "Inherit": "Hereda",
+ "HomeVideos": "Hejmaj Videoj",
+ "HeaderNextUp": "Sekva Plue",
+ "HeaderFavoriteSongs": "Favorataj Kantoj",
+ "HeaderFavoriteEpisodes": "Favorataj Epizodoj",
+ "HeaderFavoriteAlbums": "Favorataj Albumoj",
+ "Forced": "Forcita",
+ "ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita",
+ "NotificationOptionVideoPlayback": "La videoludado lanĉis",
+ "NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata",
+ "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la librara skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
+ "TaskUpdatePluginsDescription": "Elŝutas kaj instalas ĝisdatigojn por kromprogramojn, kiuj estas agorditaj por ĝisdatigi aŭtomate.",
+ "TaskDownloadMissingSubtitlesDescription": "Serĉas en interreto mankantajn subtekstojn surbaze de metadatena agordaro.",
+ "TaskRefreshPeopleDescription": "Ĝisdatigas metadatenojn por aktoroj kaj reĵisoroj en via plurmedia libraro.",
+ "TaskCleanLogsDescription": "Forigas ĵurnalajn dosierojn aĝajn pli ol {0} tagojn.",
+ "TaskRefreshLibraryDescription": "Skanas vian plurmedian libraron por novaj dosieroj kaj refreŝigas metadatenaron.",
+ "NewVersionIsAvailable": "Nova versio de Jellyfin Server estas elŝutebla.",
+ "TaskCleanCacheDescription": "Forigas stapla dosierojn ne plu necesajn de la sistemo.",
+ "TaskCleanActivityLogDescription": "Forigas aktivecan ĵurnalaĵojn pli malnovajn ol la agordita aĝo.",
+ "TaskCleanTranscodeDescription": "Forigas transkodajn dosierojn aĝajn pli ol unu tagon.",
+ "ValueHasBeenAddedToLibrary": "{0} estis aldonita al via plurmedia libraro",
+ "SubtitleDownloadFailureFromForItem": "Subtekstoj malsukcesis elŝuti de {0} por {1}",
+ "StartupEmbyServerIsLoading": "Jellyfin Server ŝarĝas. Provi denove baldaŭ.",
+ "TaskRefreshChapterImagesDescription": "Kreas bildetojn por videoj kiuj havas ĉapitrojn.",
+ "UserStoppedPlayingItemWithValues": "{0} finis ludi {1} ĉe {2}",
+ "UserPolicyUpdatedWithName": "Uzanta politiko estis ĝisdatigita por {0}",
+ "UserPasswordChangedWithName": "Pasvorto estis ŝanĝita por uzanto {0}",
+ "UserStartedPlayingItemWithValues": "{0} ludas {1} ĉe {2}",
+ "UserLockedOutWithName": "Uzanto {0} estas elŝlosita",
+ "UserOnlineFromDevice": "{0} estas enreta de {1}",
+ "UserOfflineFromDevice": "{0} malkonektis de {1}",
+ "UserDeletedWithName": "Uzanto {0} estis forigita",
+ "MessageServerConfigurationUpdated": "Servila agordaro estis ĝisdatigita",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Servila agorda sekcio {0} estis ĝisdatigita",
+ "MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}",
+ "MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita",
+ "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
+ "TaskDownloadMissingSubtitles": "Elŝutu mankantajn subtekstojn",
+ "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
+ "TaskRefreshChapterImages": "Eltiru Ĉapitro-Bildojn",
+ "TaskCleanCache": "Malplenigi Staplan Katalogon",
+ "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
+ "PluginUpdatedWithName": "{0} estis ĝisdatigita",
+ "NotificationOptionVideoPlaybackStopped": "La videoludado haltis",
+ "NotificationOptionUserLockedOut": "Uzanto ŝlosita",
+ "NotificationOptionTaskFailed": "Planita tasko malsukcesis",
+ "NotificationOptionPluginUpdateInstalled": "Ĝisdatigo de kromprogramo instalita",
+ "NotificationOptionCameraImageUploaded": "Kamera bildo alŝutita",
+ "NotificationOptionApplicationUpdateInstalled": "Aplikaĵa ĝisdatigo instalita",
+ "NotificationOptionApplicationUpdateAvailable": "Ĝisdatigo de aplikaĵo havebla",
+ "LabelRunningTimeValue": "Ludada tempo: {0}",
+ "HeaderRecordingGroups": "Rikordadaj Grupoj",
+ "FailedLoginAttemptWithUserName": "Malsukcesa ensaluta provo de {0}",
+ "CameraImageUploadedFrom": "Nova kamera bildo estis alŝutita de {0}",
+ "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis"
}
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
new file mode 100644
index 000000000..c3596ecf1
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -0,0 +1,118 @@
+{
+ "TaskCleanActivityLogDescription": "Kustutab määratud ajast vanemad tegevuslogi kirjed.",
+ "UserDownloadingItemWithValues": "{0} laeb alla {1}",
+ "HeaderRecordingGroups": "Salvestusrühmad",
+ "TaskOptimizeDatabaseDescription": "Tihendab ja puhastab andmebaasi. Selle toimingu tegemine pärast meediakogu andmebaasiga seotud muudatuste skannimist võib jõudlust parandada.",
+ "TaskOptimizeDatabase": "Optimeeri andmebaasi",
+ "TaskDownloadMissingSubtitlesDescription": "Otsib veebist puuduvaid subtiitreid vastavalt määratud metaandmete seadetele.",
+ "TaskDownloadMissingSubtitles": "Laadi alla puuduvad subtiitrid",
+ "TaskRefreshChannelsDescription": "Värskendab veebikanalite teavet.",
+ "TaskRefreshChannels": "Värskenda kanaleid",
+ "TaskCleanTranscodeDescription": "Kustutab üle ühe päeva vanused transkodeerimisfailid.",
+ "TaskCleanTranscode": "Puhasta transkoodimise kataloog",
+ "TaskUpdatePluginsDescription": "Laadib alla ja paigaldab nende pluginate uuendused, mis on seadistatud automaatselt uuenduma.",
+ "TaskUpdatePlugins": "Uuenda pluginaid",
+ "TaskRefreshPeopleDescription": "Värskendab meediakogus näitlejate ja režissööride metaandmeid.",
+ "TaskRefreshPeople": "Värskenda inimesi",
+ "TaskCleanLogsDescription": "Kustutab logifailid, mis on vanemad kui {0} päeva.",
+ "TaskCleanLogs": "Puhasta logikataloog",
+ "TaskRefreshLibraryDescription": "Otsib meedikogust uusi faile ja värskendab metaandmeid.",
+ "Collections": "Kollektsioonid",
+ "TaskRefreshLibrary": "Skaneeri meediakogu",
+ "TaskRefreshChapterImagesDescription": "Loob peatükkidega videote jaoks pisipildid.",
+ "TaskRefreshChapterImages": "Eralda peatükipildid",
+ "TaskCleanCacheDescription": "Kustutab vahemälufailid, mida süsteem enam ei vaja.",
+ "TaskCleanCache": "Puhasta vahemälu kataloog",
+ "TaskCleanActivityLog": "Puhasta tegevuslogi",
+ "TasksChannelsCategory": "Veebikanalid",
+ "TasksApplicationCategory": "Rakendus",
+ "TasksLibraryCategory": "Meediakogu",
+ "TasksMaintenanceCategory": "Hooldus",
+ "VersionNumber": "Versioon {0}",
+ "ValueSpecialEpisodeName": "Eriepisood - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
+ "UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}",
+ "UserPasswordChangedWithName": "Kasutaja {0} parooli on muudetud",
+ "UserLockedOutWithName": "Kasutaja {0} on lukustatud",
+ "UserDeletedWithName": "Kasutaja {0} on kustutatud",
+ "UserCreatedWithName": "Kasutaja {0} on loodud",
+ "ScheduledTaskStartedWithName": "{0} käivitati",
+ "ProviderValue": "Allikas: {0}",
+ "StartupEmbyServerIsLoading": "Jellyfin server laadib. Proovi varsti uuesti.",
+ "User": "Kasutaja",
+ "Undefined": "Määratlemata",
+ "TvShows": "Seriaalid",
+ "System": "Süsteem",
+ "Sync": "Sünkrooni",
+ "Songs": "Laulud",
+ "Shows": "Sarjad",
+ "ServerNameNeedsToBeRestarted": "{0} tuleb taaskäivitada",
+ "ScheduledTaskFailedWithName": "{0} nurjus",
+ "PluginUpdatedWithName": "{0} uuendati",
+ "PluginUninstalledWithName": "{0} eemaldati",
+ "PluginInstalledWithName": "{0} paigaldati",
+ "Plugin": "Plugin",
+ "Playlists": "Pleilistid",
+ "Photos": "Fotod",
+ "NotificationOptionVideoPlaybackStopped": "Video taasesitus on peatatud",
+ "NotificationOptionVideoPlayback": "Video taasesitus algas",
+ "NotificationOptionUserLockedOut": "Kasutaja on lukustatud",
+ "NotificationOptionTaskFailed": "Ajastatud ülesanne nurjus",
+ "NotificationOptionServerRestartRequired": "Vajalik on serveri taaskäivitamine",
+ "NotificationOptionPluginUpdateInstalled": "Paigaldati plugina uuendus",
+ "NotificationOptionPluginUninstalled": "Plugin eemaldati",
+ "NotificationOptionPluginInstalled": "Plugin paigaldati",
+ "NotificationOptionPluginError": "Plugina tõrge",
+ "NotificationOptionNewLibraryContent": "Lisati uut sisu",
+ "NotificationOptionInstallationFailed": "Paigaldamine nurjus",
+ "NotificationOptionCameraImageUploaded": "Kaamera pilt on üles laaditud",
+ "NotificationOptionAudioPlaybackStopped": "Heli taasesitus peatati",
+ "NotificationOptionAudioPlayback": "Heli taasesitus algas",
+ "NotificationOptionApplicationUpdateInstalled": "Rakenduse uuendus paigaldati",
+ "NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
+ "NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
+ "NameSeasonUnknown": "Tundmatu hooaeg",
+ "NameSeasonNumber": "Hooaeg {0}",
+ "NameInstallFailed": "{0} paigaldamine nurjus",
+ "MusicVideos": "Muusikavideod",
+ "Music": "Muusika",
+ "Movies": "Filmid",
+ "MixedContent": "Segatud sisu",
+ "MessageServerConfigurationUpdated": "Serveri seadistust uuendati",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Serveri seadistusosa {0} uuendati",
+ "MessageApplicationUpdatedTo": "Jellyfin server uuendati versioonile {0}",
+ "MessageApplicationUpdated": "Jellyfin server uuendati",
+ "Latest": "Uusimad",
+ "LabelRunningTimeValue": "Kestus: {0}",
+ "LabelIpAddressValue": "IP aadress: {0}",
+ "ItemRemovedWithName": "{0} eemaldati meediakogust",
+ "ItemAddedWithName": "{0} lisati meediakogusse",
+ "Inherit": "Päri",
+ "HomeVideos": "Koduvideod",
+ "HeaderNextUp": "Järgmisena",
+ "HeaderLiveTV": "Otse TV",
+ "HeaderFavoriteSongs": "Lemmiklood",
+ "HeaderFavoriteShows": "Lemmikseriaalid",
+ "HeaderFavoriteEpisodes": "Lemmikepisoodid",
+ "HeaderFavoriteArtists": "Lemmikesitajad",
+ "HeaderFavoriteAlbums": "Lemmikalbumid",
+ "HeaderContinueWatching": "Jätka vaatamist",
+ "HeaderAlbumArtists": "Albumi esitaja",
+ "Genres": "Žanrid",
+ "Forced": "Sunnitud",
+ "Folders": "Kaustad",
+ "Favorites": "Lemmikud",
+ "FailedLoginAttemptWithUserName": "Ebaõnnestunud sisselogimiskatse kasutajalt {0}",
+ "DeviceOnlineWithName": "{0} on ühendatud",
+ "DeviceOfflineWithName": "{0} katkestas ühenduse",
+ "Default": "Vaikimisi",
+ "ChapterNameValue": "Peatükk {0}",
+ "Channels": "Kanalid",
+ "CameraImageUploadedFrom": "Uus kaamera pilt laaditi üles allikalt {0}",
+ "Books": "Raamatud",
+ "AuthenticationSucceededWithUserName": "{0} autentimine õnnestus",
+ "Artists": "Esitajad",
+ "Application": "Rakendus",
+ "AppDeviceValues": "Rakendus: {0}, seade: {1}",
+ "Albums": "Albumid"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 3c51d64e0..2a56d0745 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Indéfini",
- "Forced": "Forcé"
+ "Forced": "Forcé",
+ "TaskOptimizeDatabaseDescription": "Compacte la base de données et tronque l'espace libre. Lancer cette tâche après avoir scanné la bibliothèque ou faire d'autres changements impliquant des modifications de la base peuvent ameliorer les performances.",
+ "TaskOptimizeDatabase": "Optimiser la base de données"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 0e4c38425..c3e52eb81 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -105,8 +105,8 @@
"TaskRefreshPeople": "Rafraîchir les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
- "TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
+ "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
+ "TaskRefreshLibrary": "Scanner la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index afb22ab47..b433c6f68 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -48,7 +48,7 @@
"HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteAlbums": "Álbunes Favoritos",
"HeaderContinueWatching": "Seguir mirando",
- "HeaderAlbumArtists": "Artistas de Album",
+ "HeaderAlbumArtists": "Artistas do Album",
"Genres": "Xéneros",
"Forced": "Forzado",
"Folders": "Cartafoles",
@@ -117,5 +117,7 @@
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
"UserOnlineFromDevice": "{0} está en liña desde {1}",
- "UserOfflineFromDevice": "{0} desconectouse desde {1}"
+ "UserOfflineFromDevice": "{0} desconectouse desde {1}",
+ "TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
+ "TaskOptimizeDatabase": "Optimizar base de datos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 9eb80b83b..d7cda61da 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -15,7 +15,7 @@
"Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
- "HeaderAlbumArtists": "Izvođači na albumu",
+ "HeaderAlbumArtists": "Album od izvođača",
"HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 81c1eefe7..317bdcfcb 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -119,5 +119,6 @@
"Forced": "Tvunget",
"Default": "Standard",
"TaskCleanActivityLogDescription": "Sletter oppføringer i aktivitetsloggen som er eldre enn den konfigurerte alderen.",
- "TaskOptimizeDatabase": "Optimiser database"
+ "TaskOptimizeDatabase": "Optimiser database",
+ "TaskOptimizeDatabaseDescription": "Komprimerer database og frigjør plass. Denne prosessen kan forbedre ytelsen etter skanning av bibliotek eller andre handlinger som fører til databaseendringer."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index f79840c78..79f921bcb 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -15,7 +15,7 @@
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
- "HeaderAlbumArtists": "Albumartiesten",
+ "HeaderAlbumArtists": "Artiests Album",
"HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index 0967ef424..e3a3bfaf1 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -1 +1,5 @@
-{}
+{
+ "Books": "Libros",
+ "AuthenticationSucceededWithUserName": "{0} autentificado correctamente",
+ "Artists": "Artistas"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 474dacd7c..a9dbd53ea 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -1,6 +1,6 @@
{
- "HeaderLiveTV": "TV em Directo",
- "Collections": "Colecções",
+ "HeaderLiveTV": "TV Ao Vivo",
+ "Collections": "Coleções",
"Books": "Livros",
"Artists": "Artistas",
"Albums": "Álbuns",
@@ -10,29 +10,29 @@
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
- "HeaderContinueWatching": "Continuar a Assistir",
+ "HeaderContinueWatching": "Continuar assistindo",
"HeaderAlbumArtists": "Artistas do Álbum",
- "Genres": "Géneros",
- "Folders": "Directórios",
+ "Genres": "Gêneros",
+ "Folders": "Diretórios",
"Favorites": "Favoritos",
"Channels": "Canais",
- "UserDownloadingItemWithValues": "{0} está a ser transferido {1}",
+ "UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
"VersionNumber": "Versão {0}",
"ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
- "UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
- "UserPolicyUpdatedWithName": "A política do utilizador {0} foi alterada",
- "UserPasswordChangedWithName": "A palavra-passe do utilizador {0} foi alterada",
- "UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
+ "UserStartedPlayingItemWithValues": "{0} está reproduzindo {1} em {2}",
+ "UserPolicyUpdatedWithName": "A política do usuário {0} foi alterada",
+ "UserPasswordChangedWithName": "A senha do usuário {0} foi alterada",
+ "UserOnlineFromDevice": "{0} está online a partir de {1}",
"UserOfflineFromDevice": "{0} desconectou-se a partir de {1}",
- "UserLockedOutWithName": "O utilizador {0} foi bloqueado",
- "UserDeletedWithName": "O utilizador {0} foi removido",
- "UserCreatedWithName": "O utilizador {0} foi criado",
- "User": "Utilizador",
+ "UserLockedOutWithName": "O usuário {0} foi bloqueado",
+ "UserDeletedWithName": "O usuário {0} foi removido",
+ "UserCreatedWithName": "O usuário {0} foi criado",
+ "User": "Usuário",
"TvShows": "Séries",
"System": "Sistema",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
- "StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente dentro de momentos.",
+ "StartupEmbyServerIsLoading": "O servidor Jellyfin está iniciando. Tente novamente dentro de momentos.",
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciado",
"ScheduledTaskStartedWithName": "{0} iniciou",
"ScheduledTaskFailedWithName": "{0} falhou",
@@ -43,38 +43,38 @@
"Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
- "NotificationOptionUserLockedOut": "Utilizador bloqueado",
- "NotificationOptionTaskFailed": "Falha em tarefa agendada",
+ "NotificationOptionUserLockedOut": "Usuário bloqueado",
+ "NotificationOptionTaskFailed": "Falha na tarefa agendada",
"NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor",
- "NotificationOptionPluginUpdateInstalled": "Plugin actualizado",
+ "NotificationOptionPluginUpdateInstalled": "Plugin atualizado",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
"NotificationOptionPluginInstalled": "Plugin instalado",
"NotificationOptionPluginError": "Falha no plugin",
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
"NotificationOptionInstallationFailed": "Falha de instalação",
- "NotificationOptionCameraImageUploaded": "Imagem de câmara enviada",
+ "NotificationOptionCameraImageUploaded": "Imagem de câmera enviada",
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
- "NotificationOptionApplicationUpdateInstalled": "A actualização da aplicação foi instalada",
- "NotificationOptionApplicationUpdateAvailable": "Uma actualização da aplicação está disponível",
- "NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para transferência.",
+ "NotificationOptionApplicationUpdateInstalled": "A atualização do aplicativo foi instalada",
+ "NotificationOptionApplicationUpdateAvailable": "Uma atualização do aplicativo está disponível",
+ "NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.",
"NameSeasonUnknown": "Temporada Desconhecida",
"NameSeasonNumber": "Temporada {0}",
"NameInstallFailed": "Falha na instalação de {0}",
"MusicVideos": "Videoclipes",
"Music": "Música",
- "MixedContent": "Conteúdo Misto",
- "MessageServerConfigurationUpdated": "A configuração do servidor foi actualizada",
- "MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na secção {0} foram atualizadas",
+ "MixedContent": "Conteúdo diverso",
+ "MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada",
+ "MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na seção {0} foram atualizadas",
"MessageApplicationUpdatedTo": "O servidor Jellyfin foi atualizado para a versão {0}",
- "MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
+ "MessageApplicationUpdated": "O servidor Jellyfin foi atualizado",
"Latest": "Mais Recente",
"LabelRunningTimeValue": "Duração: {0}",
"LabelIpAddressValue": "Endereço de IP: {0}",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"Inherit": "Herdar",
- "HomeVideos": "Vídeos Caseiros",
+ "HomeVideos": "Vídeos principais",
"HeaderRecordingGroups": "Grupos de Gravação",
"ValueSpecialEpisodeName": "Episódio Especial - {0}",
"Sync": "Sincronização",
@@ -83,22 +83,22 @@
"Playlists": "Listas de Reprodução",
"Photos": "Fotografias",
"Movies": "Filmes",
- "FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}",
- "DeviceOnlineWithName": "{0} está connectado",
+ "FailedLoginAttemptWithUserName": "Tentativa falha de login a partir de {0}",
+ "DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} desconectou-se",
"ChapterNameValue": "Capítulo {0}",
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
- "Application": "Aplicação",
- "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
+ "Application": "Aplicativo",
+ "AppDeviceValues": "Aplicativo {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
- "TasksApplicationCategory": "Aplicação",
+ "TasksApplicationCategory": "Aplicativo",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskRefreshChannels": "Atualizar Canais",
"TaskUpdatePlugins": "Atualizar Plugins",
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
- "TaskCleanLogs": "Limpar diretório de log",
+ "TaskCleanLogs": "Limpar diretório de logs",
"TaskRefreshLibrary": "Escanear biblioteca de mídias",
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
@@ -109,14 +109,15 @@
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
"TaskCleanTranscode": "Limpar o diretório de Transcode",
- "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
+ "TaskUpdatePluginsDescription": "Baixa e instala as atualizações para plug-ins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
"TaskRefreshPeople": "Atualizar pessoas",
- "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados.",
- "TaskCleanActivityLog": "Limpar registo de atividade",
+ "TaskRefreshLibraryDescription": "Pesquisa sua biblioteca de media por novos arquivos e atualiza os metadados.",
+ "TaskCleanActivityLog": "Limpar registro de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Predefinição",
"TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
- "TaskOptimizeDatabase": "Otimizar base de dados"
+ "TaskOptimizeDatabase": "Otimizar base de dados",
+ "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index e36fdc43d..066ef5587 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -74,7 +74,7 @@
"NameSeasonUnknown": "Sezon i panjohur",
"NameSeasonNumber": "Sezoni {0}",
"NameInstallFailed": "Instalimi i {0} dështoi",
- "MusicVideos": "Videot muzikore",
+ "MusicVideos": "Video muzikore",
"Music": "Muzikë",
"Movies": "Filmat",
"MixedContent": "Përmbajtje e përzier",
@@ -96,7 +96,7 @@
"HeaderFavoriteArtists": "Artistët e preferuar",
"HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
- "HeaderAlbumArtists": "Artistët e albumeve",
+ "HeaderAlbumArtists": "Artistët e Albumeve",
"Genres": "Zhanret",
"Folders": "Skedarët",
"Favorites": "Të preferuarat",
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 5a2069df5..e99ed6f0b 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -1,5 +1,5 @@
{
- "MusicVideos": "Музичні відеокліпи",
+ "MusicVideos": "Відеокліпи",
"Music": "Музика",
"Movies": "Фільми",
"MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
@@ -16,7 +16,7 @@
"HeaderFavoriteArtists": "Улюблені виконавці",
"HeaderFavoriteAlbums": "Улюблені альбоми",
"HeaderContinueWatching": "Продовжити перегляд",
- "HeaderAlbumArtists": "Виконавці альбому",
+ "HeaderAlbumArtists": "Виконавці альбомів",
"Genres": "Жанри",
"Folders": "Каталоги",
"Favorites": "Улюблені",
@@ -38,7 +38,7 @@
"NotificationOptionPluginInstalled": "Плагін встановлено",
"NotificationOptionPluginError": "Помилка плагіна",
"NotificationOptionNewLibraryContent": "Додано новий контент",
- "HomeVideos": "Домашнє відео",
+ "HomeVideos": "Мої відео",
"FailedLoginAttemptWithUserName": "Невдала спроба входу від {0}",
"LabelRunningTimeValue": "Тривалість: {0}",
"TaskDownloadMissingSubtitlesDescription": "Шукає в Інтернеті відсутні субтитри на основі конфігурації метаданих.",
@@ -117,5 +117,7 @@
"TaskCleanActivityLogDescription": "Видаляє старші за встановлений термін записи з журналу активності.",
"TaskCleanActivityLog": "Очистити журнал активності",
"Undefined": "Не визначено",
- "Default": "За замовчуванням"
+ "Default": "За замовчуванням",
+ "TaskOptimizeDatabase": "Оптимізувати базу даних",
+ "TaskOptimizeDatabaseDescription": "Стиснення бази даних та збільшення вільного простору. Виконання цього завдання після сканування бібліотеки або внесення інших змін, які передбачають модифікацію бази даних, може покращити продуктивність."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index 5d6d0775c..11af9fc98 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -90,7 +90,7 @@
"NameSeasonUnknown": "نامعلوم باب",
"NameSeasonNumber": "باب {0}",
"NameInstallFailed": "{0} تنصیب ناکام ہوگئی",
- "MusicVideos": "موسیقی ویڈیو",
+ "MusicVideos": "ویڈیو موسیقی",
"Music": "موسیقی",
"MixedContent": "مخلوط مواد",
"MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
@@ -99,18 +99,19 @@
"MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
"Latest": "تازہ ترین",
"LabelRunningTimeValue": "چلانے کی مدت",
- "LabelIpAddressValue": "ای پی پتے {0}",
+ "LabelIpAddressValue": "آئ پی ایڈریس {0}",
"ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
"ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
"Inherit": "وراثت میں",
"HomeVideos": "ہوم ویڈیو",
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
- "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
+ "FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
"ChapterNameValue": "باب",
"AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
"CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
"Application": "پروگرام",
- "AppDeviceValues": "پروگرام:{0}, آلہ:{1}"
+ "AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
+ "Forced": "جَبری"
}
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 8aaa1f7bb..ac6606d39 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -23,7 +23,6 @@ namespace Emby.Server.Implementations.MediaEncoder
{
public class EncodingManager : IEncodingManager
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IFileSystem _fileSystem;
private readonly ILogger<EncodingManager> _logger;
private readonly IMediaEncoder _encoder;
@@ -193,7 +192,7 @@ namespace Emby.Server.Implementations.MediaEncoder
private string GetChapterImagePath(Video video, long chapterPositionTicks)
{
- var filename = video.DateModified.Ticks.ToString(_usCulture) + "_" + chapterPositionTicks.ToString(_usCulture) + ".jpg";
+ var filename = video.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
return Path.Combine(GetChapterImagesPath(video), filename);
}
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index 9b799e854..0c451ccb6 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -16,11 +16,7 @@ namespace Emby.Server.Implementations.Net
public sealed class UdpSocket : ISocket, IDisposable
{
- private Socket _socket;
private readonly int _localPort;
- private bool _disposed = false;
-
- public Socket Socket => _socket;
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
{
@@ -32,6 +28,8 @@ namespace Emby.Server.Implementations.Net
SocketFlags = SocketFlags.None
};
+ private Socket _socket;
+ private bool _disposed = false;
private TaskCompletionSource<SocketReceiveResult> _currentReceiveTaskCompletionSource;
private TaskCompletionSource<int> _currentSendTaskCompletionSource;
@@ -64,6 +62,8 @@ namespace Emby.Server.Implementations.Net
InitReceiveSocketAsyncEventArgs();
}
+ public Socket Socket => _socket;
+
public IPAddress LocalIPAddress { get; }
private void InitReceiveSocketAsyncEventArgs()
diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index 4160f3a50..8b1cee89d 100644
--- a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
@@ -17,6 +17,15 @@ namespace Emby.Server.Implementations.Playlists
Name = "Playlists";
}
+ [JsonIgnore]
+ public override bool IsHidden => true;
+
+ [JsonIgnore]
+ public override bool SupportsInheritedParentImages => false;
+
+ [JsonIgnore]
+ public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
+
public override bool IsVisible(User user)
{
return base.IsVisible(user) && GetChildren(user, true).Any();
@@ -27,15 +36,6 @@ namespace Emby.Server.Implementations.Playlists
return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>();
}
- [JsonIgnore]
- public override bool IsHidden => true;
-
- [JsonIgnore]
- public override bool SupportsInheritedParentImages => false;
-
- [JsonIgnore]
- public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
-
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (query.User == null)
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index b8e1dc2c0..d52c0b2a1 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -8,10 +8,10 @@ using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
@@ -39,14 +39,6 @@ namespace Emby.Server.Implementations.Plugins
private IHttpClientFactory? _httpClientFactory;
- private IHttpClientFactory HttpClientFactory
- {
- get
- {
- return _httpClientFactory ?? (_httpClientFactory = _appHost.Resolve<IHttpClientFactory>());
- }
- }
-
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
@@ -86,6 +78,14 @@ namespace Emby.Server.Implementations.Plugins
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
}
+ private IHttpClientFactory HttpClientFactory
+ {
+ get
+ {
+ return _httpClientFactory ??= _appHost.Resolve<IHttpClientFactory>();
+ }
+ }
+
/// <summary>
/// Gets the Plugins.
/// </summary>
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index ae773c658..c81c26994 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.QuickConnect
/// <summary>
/// Quick connect implementation.
/// </summary>
- public class QuickConnectManager : IQuickConnect, IDisposable
+ public class QuickConnectManager : IQuickConnect
{
/// <summary>
/// The length of user facing codes.
@@ -30,7 +30,6 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
private const int Timeout = 10;
- private readonly RNGCryptoServiceProvider _rng = new ();
private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
@@ -140,7 +139,7 @@ namespace Emby.Server.Implementations.QuickConnect
uint scale = uint.MaxValue;
while (scale == uint.MaxValue)
{
- _rng.GetBytes(raw);
+ RandomNumberGenerator.Fill(raw);
scale = BitConverter.ToUInt32(raw);
}
@@ -199,31 +198,10 @@ namespace Emby.Server.Implementations.QuickConnect
return result.AuthenticationResult;
}
- /// <summary>
- /// Dispose.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Dispose.
- /// </summary>
- /// <param name="disposing">Dispose unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- _rng.Dispose();
- }
- }
-
private string GenerateSecureRandom(int length = 32)
{
Span<byte> bytes = stackalloc byte[length];
- _rng.GetBytes(bytes);
+ RandomNumberGenerator.Fill(bytes);
return Convert.ToHexString(bytes);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index fb93c375d..f2cdfeb16 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -10,9 +10,9 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -25,6 +25,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class ScheduledTaskWorker : IScheduledTaskWorker
{
/// <summary>
+ /// The options for the json Serializer.
+ /// </summary>
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+
+ /// <summary>
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
@@ -67,11 +72,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
private string _id;
/// <summary>
- /// The options for the json Serializer.
- /// </summary>
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
-
- /// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
/// </summary>
/// <param name="scheduledTask">The scheduled task.</param>
@@ -365,7 +365,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
/// <param name="options">Task options.</param>
/// <returns>Task.</returns>
- /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running</exception>
+ /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception>
public async Task Execute(TaskOptions options)
{
var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 4f0df75bf..0431858fc 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -19,16 +19,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class TaskManager : ITaskManager
{
- public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
-
- public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
-
- /// <summary>
- /// Gets the list of Scheduled Tasks.
- /// </summary>
- /// <value>The scheduled tasks.</value>
- public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
-
/// <summary>
/// The _task queue.
/// </summary>
@@ -53,10 +43,20 @@ namespace Emby.Server.Implementations.ScheduledTasks
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
}
+ public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+
+ public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+
+ /// <summary>
+ /// Gets the list of Scheduled Tasks.
+ /// </summary>
+ /// <value>The scheduled tasks.</value>
+ public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
+
/// <summary>
/// Cancels if running and queue.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The task type.</typeparam>
/// <param name="options">Task options.</param>
public void CancelIfRunningAndQueue<T>(TaskOptions options)
where T : IScheduledTask
@@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Cancels if running.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The task type.</typeparam>
public void CancelIfRunning<T>()
where T : IScheduledTask
{
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Queues the scheduled task.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The task type.</typeparam>
/// <param name="options">Task options.</param>
public void QueueScheduledTask<T>(TaskOptions options)
where T : IScheduledTask
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index b764a139c..09ea6271d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -39,6 +39,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
/// </summary>
+ /// <param name="libraryManager">The library manager.</param>.
+ /// <param name="itemRepo">The item repository.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <param name="encodingManager">The encoding manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="localization">The localization manager.</param>
public ChapterImagesTask(
ILibraryManager libraryManager,
IItemRepository itemRepo,
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 1ad1d0f50..35a4aeef6 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -22,6 +22,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <summary>
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="localization">The localization manager.</param>
+ /// <param name="provider">The jellyfin DB context provider.</param>
public OptimizeDatabaseTask(
ILogger<OptimizeDatabaseTask> logger,
ILocalizationManager localization,
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index 57d294a40..53c692a46 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -32,9 +32,24 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
+ public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
+
+ public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
+
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ public string Key => "RefreshPeople";
+
+ public bool IsHidden => false;
+
+ public bool IsEnabled => true;
+
+ public bool IsLogged => true;
+
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
+ /// <returns>An <see cref="IEnumerable{TaskTriggerInfo}"/> containing the default trigger infos for this task.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
@@ -57,19 +72,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
return _libraryManager.ValidatePeople(cancellationToken, progress);
}
-
- public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
-
- public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
-
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
-
- public string Key => "RefreshPeople";
-
- public bool IsHidden => false;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index 51b620404..2184b3d03 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -33,6 +33,18 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ /// <inheritdoc />
+ public string Key => "RefreshLibrary";
+
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
@@ -60,26 +72,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
}
-
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
-
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
-
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
-
- /// <inheritdoc />
- public string Key => "RefreshLibrary";
-
- /// <inheritdoc />
- public bool IsHidden => false;
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index 6cf9a8f71..369a2b0d8 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -12,6 +12,11 @@ namespace Emby.Server.Implementations
/// <summary>
/// Initializes a new instance of the <see cref="ServerApplicationPaths" /> class.
/// </summary>
+ /// <param name="programDataPath">The path for Jellyfin's data.</param>
+ /// <param name="logDirectoryPath">The path for Jellyfin's logging directory.</param>
+ /// <param name="configurationDirectoryPath">The path for Jellyfin's configuration directory.</param>
+ /// <param name="cacheDirectoryPath">The path for Jellyfin's cache directory.</param>
+ /// <param name="webDirectoryPath">The path for Jellyfin's web UI.</param>
public ServerApplicationPaths(
string programDataPath,
string logDirectoryPath,
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 334ce5c9d..341d2c8e6 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -741,6 +741,8 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Used to report playback progress for an item.
/// </summary>
+ /// <param name="info">The playback progress info.</param>
+ /// <param name="isAutomated">Whether this is an automated update.</param>
/// <returns>Task.</returns>
public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
{
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 2b0ab536f..db8b68949 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class AiredEpisodeOrderComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.AiredEpisodeOrder;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -28,16 +34,6 @@ namespace Emby.Server.Implementations.Sorting
throw new ArgumentNullException(nameof(y));
}
- if (x.PremiereDate.HasValue && y.PremiereDate.HasValue)
- {
- var val = DateTime.Compare(x.PremiereDate.Value, y.PremiereDate.Value);
-
- if (val != 0)
- {
- // return val;
- }
- }
-
var episode1 = x as Episode;
var episode2 = y as Episode;
@@ -156,14 +152,14 @@ namespace Emby.Server.Implementations.Sorting
{
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
+ var comparisonResult = xValue.CompareTo(yValue);
+ // If equal, compare premiere dates
+ if (comparisonResult == 0 && x.PremiereDate.HasValue && y.PremiereDate.HasValue)
+ {
+ comparisonResult = DateTime.Compare(x.PremiereDate.Value, y.PremiereDate.Value);
+ }
- return xValue.CompareTo(yValue);
+ return comparisonResult;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.AiredEpisodeOrder;
}
}
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 42e644970..bd1966623 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -13,6 +13,12 @@ namespace Emby.Server.Implementations.Sorting
public class AlbumArtistComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.AlbumArtist;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -34,11 +40,5 @@ namespace Emby.Server.Implementations.Sorting
return audio?.AlbumArtists.FirstOrDefault();
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.AlbumArtist;
}
}
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 1db3f5e9c..fe7dc84cb 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -12,6 +12,12 @@ namespace Emby.Server.Implementations.Sorting
public class AlbumComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Album;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -33,11 +39,5 @@ namespace Emby.Server.Implementations.Sorting
return audio == null ? string.Empty : audio.Album;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Album;
}
}
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index d20dedc2d..ba1835e4f 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -10,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting
public class CriticRatingComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.CriticRating;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -24,11 +30,5 @@ namespace Emby.Server.Implementations.Sorting
{
return x?.CriticRating ?? 0;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.CriticRating;
}
}
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index d3f10f78c..8b460166c 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class DateCreatedComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.DateCreated;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -30,11 +36,5 @@ namespace Emby.Server.Implementations.Sorting
return DateTime.Compare(x.DateCreated, y.DateCreated);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.DateCreated;
}
}
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 08a44319f..ec818253b 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -33,6 +33,12 @@ namespace Emby.Server.Implementations.Sorting
public IUserDataManager UserDataRepository { get; set; }
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.DatePlayed;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -59,11 +65,5 @@ namespace Emby.Server.Implementations.Sorting
return DateTime.MinValue;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.DatePlayed;
}
}
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index 4de81a69e..8f87717f4 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class NameComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Name;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -30,11 +36,5 @@ namespace Emby.Server.Implementations.Sorting
return string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Name;
}
}
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 04e4865cb..45c9044c5 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -20,6 +20,24 @@ namespace Emby.Server.Implementations.Sorting
public User User { get; set; }
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.PlayCount;
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -41,23 +59,5 @@ namespace Emby.Server.Implementations.Sorting
return userdata == null ? 0 : userdata.PlayCount;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.PlayCount;
-
- /// <summary>
- /// Gets or sets the user data repository.
- /// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
-
- /// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- public IUserManager UserManager { get; set; }
}
}
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index c98f97bf1..b217556ef 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class PremiereDateComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.PremiereDate;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -52,11 +58,5 @@ namespace Emby.Server.Implementations.Sorting
return DateTime.MinValue;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.PremiereDate;
}
}
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index df9f9957d..d2022df7a 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -10,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting
public class ProductionYearComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.ProductionYear;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -44,11 +50,5 @@ namespace Emby.Server.Implementations.Sorting
return 0;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.ProductionYear;
}
}
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index af3bc2750..bf0168222 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class RandomComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Random;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -20,11 +26,5 @@ namespace Emby.Server.Implementations.Sorting
{
return Guid.NewGuid().CompareTo(Guid.NewGuid());
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Random;
}
}
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index 129315303..e32e5552e 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -13,6 +13,12 @@ namespace Emby.Server.Implementations.Sorting
public class RuntimeComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Runtime;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -32,11 +38,5 @@ namespace Emby.Server.Implementations.Sorting
return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Runtime;
}
}
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index 8d30716d3..fb97a0349 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -13,6 +13,12 @@ namespace Emby.Server.Implementations.Sorting
public class SortNameComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.SortName;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -32,11 +38,5 @@ namespace Emby.Server.Implementations.Sorting
return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.SortName;
}
}
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 6826aee3b..4d89cfa8b 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -14,6 +14,12 @@ namespace Emby.Server.Implementations.Sorting
public class StudioComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Studio;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -33,11 +39,5 @@ namespace Emby.Server.Implementations.Sorting
return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault());
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Studio;
}
}
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 8179e26c5..bf51c3968 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -29,10 +29,10 @@ namespace Emby.Server.Implementations.Udp
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
- private Socket _udpSocket;
- private IPEndPoint _endpoint;
private readonly byte[] _receiveBuffer = new byte[8192];
+ private Socket _udpSocket;
+ private IPEndPoint _endpoint;
private bool _disposed = false;
/// <summary>
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 7b0afa4e2..4a022c5db 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -10,8 +10,8 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
-using MediaBrowser.Common.Configuration;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
index 9815e252e..dd0bd4ec2 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
@@ -32,18 +32,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
}
/// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement)
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement)
{
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
- context.Succeed(firstTimeSetupOrDefaultRequirement);
+ context.Succeed(requirement);
return Task.CompletedTask;
}
var validated = ValidateClaims(context.User);
if (validated)
{
- context.Succeed(firstTimeSetupOrDefaultRequirement);
+ context.Succeed(requirement);
}
else
{
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
index decbe0c03..90b76ee99 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
@@ -33,18 +33,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
}
/// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement firstTimeSetupOrElevatedRequirement)
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement)
{
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
- context.Succeed(firstTimeSetupOrElevatedRequirement);
+ context.Succeed(requirement);
return Task.CompletedTask;
}
var validated = ValidateClaims(context.User);
if (validated && context.User.IsInRole(UserRoles.Administrator))
{
- context.Succeed(firstTimeSetupOrElevatedRequirement);
+ context.Succeed(requirement);
}
else
{
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
index b898ac76c..e6c04eb08 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
@@ -51,7 +51,7 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
- || _syncPlayManager.IsUserActive(userId!.Value))
+ || _syncPlayManager.IsUserActive(userId.Value))
{
context.Succeed(requirement);
}
@@ -85,7 +85,7 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{
- if (_syncPlayManager.IsUserActive(userId!.Value))
+ if (_syncPlayManager.IsUserActive(userId.Value))
{
context.Succeed(requirement);
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 445733c24..87cb418d9 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
if (enableInMainMenu.HasValue)
{
- configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList();
+ configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
}
return configPages;
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 87b4577b6..2079476d0 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -137,7 +137,7 @@ namespace Jellyfin.Api.Controllers
}
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
- existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
+ existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index a54003357..42e82dd5b 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1709,7 +1709,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext);
}
private long GetEndPositionTicks(StreamState state, int requestedIndex)
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 473bdc523..71caa0fe0 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Invalid segment.");
}
- return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file), false, HttpContext);
}
/// <summary>
@@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path), false, HttpContext);
}
}
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 9dc280e13..86933074d 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
@@ -153,7 +153,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
@@ -341,7 +341,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
@@ -377,7 +377,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
@@ -2007,7 +2007,7 @@ namespace Jellyfin.Api.Controllers
Response.Headers.Add(HeaderNames.CacheControl, "public");
}
- Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
+ Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
// if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
@@ -2026,7 +2026,7 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
- return PhysicalFile(imagePath, imageContentType);
+ return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
}
}
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 4774ed4ef..a6c2e07c9 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -80,7 +80,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -223,7 +223,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -259,7 +259,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -332,7 +332,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 64d7b2f3e..fd137f98f 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -263,8 +263,8 @@ namespace Jellyfin.Api.Controllers
item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
}
- item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null;
- item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null;
+ item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
+ item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
item.ProductionYear = request.ProductionYear;
item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
item.CustomRating = request.CustomRating;
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 52eefc5c2..f0d44e5cc 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
{
- var query = new InternalItemsQuery(user!)
+ var query = new InternalItemsQuery(user)
{
IsPlayed = isPlayed,
MediaTypes = mediaTypes,
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 93dc76729..b131530c9 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1199,15 +1199,15 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
- public async Task<ActionResult> GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
+ public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
{
- var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
if (liveStreamInfo == null)
{
return NotFound();
}
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 7c78928f7..b422eb78c 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -184,7 +184,7 @@ namespace Jellyfin.Api.Controllers
audioStreamIndex,
subtitleStreamIndex,
maxAudioChannels,
- info!.PlaySessionId!,
+ info.PlaySessionId!,
userId ?? Guid.Empty,
enableDirectPlay.Value,
enableDirectStream.Value,
@@ -316,7 +316,7 @@ namespace Jellyfin.Api.Controllers
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
- new Random().NextBytes(buffer);
+ Random.Shared.NextBytes(buffer);
return File(buffer, MediaTypeNames.Application.Octet);
}
finally
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 7a2c23991..35921ede8 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -7,6 +7,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -182,36 +183,5 @@ namespace Jellyfin.Api.Controllers
{
return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
}
-
- /// <summary>
- /// Downloads the image.
- /// </summary>
- /// <param name="url">The URL.</param>
- /// <param name="urlHash">The URL hash.</param>
- /// <param name="pointerCachePath">The pointer cache path.</param>
- /// <returns>Task.</returns>
- private async Task DownloadImage(Uri url, Guid urlHash, string pointerCachePath)
- {
- var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
- using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
- if (response.Content.Headers.ContentType?.MediaType == null)
- {
- throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
- }
-
- var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
- var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
- var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
- Directory.CreateDirectory(fullCacheDirectory);
- // 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, AsyncFile.UseAsyncIO);
- await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
-
- var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
- Directory.CreateDirectory(pointerCacheDirectory);
- await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
- .ConfigureAwait(false);
- }
}
}
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 11f67ee89..db8307f28 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers
{
var video = (Video)_libraryManager.GetItemById(itemId);
- return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false);
+ return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
@@ -376,7 +376,7 @@ namespace Jellyfin.Api.Controllers
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
var url = string.Format(
- CultureInfo.CurrentCulture,
+ CultureInfo.InvariantCulture,
"stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
positionTicks.ToString(CultureInfo.InvariantCulture),
endPositionTicks.ToString(CultureInfo.InvariantCulture),
@@ -417,6 +417,8 @@ namespace Jellyfin.Api.Controllers
IsForced = body.IsForced,
Stream = memoryStream
}).ConfigureAwait(false);
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index e6584f0fe..904738bb4 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<SystemInfo> GetSystemInfo()
{
- return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
+ return _appHost.GetSystemInfo(Request);
}
/// <summary>
@@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{
- return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
+ return _appHost.GetPublicSystemInfo(Request);
}
/// <summary>
@@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers
// For older files, assume fully static
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
- FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
return File(stream, "text/plain; charset=utf-8");
}
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index 7df51c7af..e7c5a7125 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -21,10 +21,10 @@ namespace Jellyfin.Api.Controllers
public ActionResult<UtcTimeResponse> GetUtcTime()
{
// Important to keep the following line at the beginning
- var requestReceptionTime = DateTime.UtcNow.ToUniversalTime();
+ var requestReceptionTime = DateTime.UtcNow;
// Important to keep the following line at the end
- var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime();
+ var responseTransmissionTime = DateTime.UtcNow;
// Implementing NTP on such a high level results in this useless
// information being sent. On the other hand it enables future additions.
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 7c5b8a43b..5dd773331 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
{
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@@ -147,13 +147,13 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value)
: null;
- var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
+ var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
var parentIdGuid = parentId ?? Guid.Empty;
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -223,7 +223,7 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{
@@ -350,7 +350,7 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 20a02bf4a..bc9527a0b 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers
null,
null,
maxAudioChannels,
- info!.PlaySessionId!,
+ info.PlaySessionId!,
userId ?? Guid.Empty,
true,
true,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index bc6fc904a..e1cbc6f33 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -453,14 +453,15 @@ namespace Jellyfin.Api.Controllers
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
- await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo == null)
{
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
+ return NotFound();
+ }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
+ return File(liveStream, MimeTypes.GetMimeType("file.ts"));
}
// Static remote stream
@@ -492,13 +493,8 @@ namespace Jellyfin.Api.Controllers
if (state.MediaSource.IsInfiniteStream)
{
- await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- return File(Response.Body, contentType);
+ var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return File(liveStream, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index ddcde1cf6..bec961dad 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -1,4 +1,5 @@
-using System.Net.Http;
+using System.IO;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
@@ -120,14 +121,15 @@ namespace Jellyfin.Api.Helpers
{
StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
- await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo == null)
+ {
+ throw new FileNotFoundException();
+ }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, MimeTypes.GetMimeType("file.ts")!);
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts"));
}
// Static remote stream
@@ -145,7 +147,7 @@ namespace Jellyfin.Api.Helpers
}
var outputPath = state.OutputFilePath;
- var outputPathExists = System.IO.File.Exists(outputPath);
+ var outputPathExists = File.Exists(outputPath);
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var isTranscodeCached = outputPathExists && transcodingJob != null;
@@ -159,13 +161,8 @@ namespace Jellyfin.Api.Helpers
if (state.MediaSource.IsInfiniteStream)
{
- await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType);
+ var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
index 29e6b4193..c1c2f93b4 100644
--- a/Jellyfin.Api/Helpers/ClaimHelpers.cs
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Helpers
var value = GetClaimValue(user, InternalClaimTypes.UserId);
return string.IsNullOrEmpty(value)
? null
- : (Guid?)Guid.Parse(value);
+ : Guid.Parse(value);
}
/// <summary>
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index b0fd59e5e..6385b62c9 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
@@ -40,7 +41,7 @@ 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), cancellationToken).ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType?.ToString();
+ var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index f36769dc2..456762147 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -38,7 +38,7 @@ namespace Jellyfin.Api.Helpers
FileAccess.Read,
FileShare.ReadWrite,
IODefaults.FileStreamBufferSize,
- (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan);
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
using var reader = new StreamReader(fileStream);
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
deleted file mode 100644
index 81970b041..000000000
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ /dev/null
@@ -1,187 +0,0 @@
-using System;
-using System.Buffers;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Api.Models.PlaybackDtos;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.IO;
-
-namespace Jellyfin.Api.Helpers
-{
- /// <summary>
- /// Progressive file copier.
- /// </summary>
- public class ProgressiveFileCopier
- {
- private readonly TranscodingJobDto? _job;
- private readonly string? _path;
- private readonly CancellationToken _cancellationToken;
- private readonly IDirectStreamProvider? _directStreamProvider;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private long _bytesWritten;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
- /// </summary>
- /// <param name="path">The path to copy from.</param>
- /// <param name="job">The transcoding job.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
- {
- _path = path;
- _job = job;
- _cancellationToken = cancellationToken;
- _transcodingJobHelper = transcodingJobHelper;
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
- /// </summary>
- /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
- /// <param name="job">The transcoding job.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
- {
- _directStreamProvider = directStreamProvider;
- _job = job;
- _cancellationToken = cancellationToken;
- _transcodingJobHelper = transcodingJobHelper;
- }
-
- /// <summary>
- /// Gets or sets a value indicating whether allow read end of file.
- /// </summary>
- public bool AllowEndOfFile { get; set; } = true;
-
- /// <summary>
- /// Gets or sets copy start position.
- /// </summary>
- public long StartPosition { get; set; }
-
- /// <summary>
- /// Write source stream to output.
- /// </summary>
- /// <param name="outputStream">Output stream.</param>
- /// <param name="cancellationToken">Cancellation token.</param>
- /// <returns>A <see cref="Task"/>.</returns>
- public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
- {
- using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken);
- cancellationToken = linkedCancellationTokenSource.Token;
-
- try
- {
- if (_directStreamProvider != null)
- {
- await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
- return;
- }
-
- var fileOptions = FileOptions.SequentialScan;
- var allowAsyncFileRead = false;
-
- if (AsyncFile.UseAsyncIO)
- {
- fileOptions |= FileOptions.Asynchronous;
- allowAsyncFileRead = true;
- }
-
- if (_path == null)
- {
- throw new ResourceNotFoundException(nameof(_path));
- }
-
- await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
-
- var eofCount = 0;
- const int EmptyReadLimit = 20;
- if (StartPosition > 0)
- {
- inputStream.Position = StartPosition;
- }
-
- while (eofCount < EmptyReadLimit || !AllowEndOfFile)
- {
- var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
-
- if (bytesRead == 0)
- {
- if (_job == null || _job.HasExited)
- {
- eofCount++;
- }
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- eofCount = 0;
- }
- }
- }
- finally
- {
- if (_job != null)
- {
- _transcodingJobHelper.OnTranscodeEndRequest(_job);
- }
- }
- }
-
- private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
- {
- var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
- try
- {
- int bytesRead;
- int totalBytesRead = 0;
-
- if (readAsync)
- {
- bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = source.Read(array, 0, array.Length);
- }
-
- while (bytesRead != 0)
- {
- var bytesToWrite = bytesRead;
-
- if (bytesToWrite > 0)
- {
- await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
- _bytesWritten += bytesRead;
- totalBytesRead += bytesRead;
-
- if (_job != null)
- {
- _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
- }
- }
-
- if (readAsync)
- {
- bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = source.Read(array, 0, array.Length);
- }
- }
-
- return totalBytesRead;
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(array);
- }
- }
- }
-}
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index d4cc0172d..61e18220a 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -13,11 +13,10 @@ namespace Jellyfin.Api.Helpers
/// </summary>
public class ProgressiveFileStream : Stream
{
- private readonly FileStream _fileStream;
+ private readonly Stream _stream;
private readonly TranscodingJobDto? _job;
- private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly TranscodingJobHelper? _transcodingJobHelper;
private readonly int _timeoutMs;
- private readonly bool _allowAsyncFileRead;
private int _bytesWritten;
private bool _disposed;
@@ -33,23 +32,25 @@ namespace Jellyfin.Api.Helpers
_job = job;
_transcodingJobHelper = transcodingJobHelper;
_timeoutMs = timeoutMs;
- _bytesWritten = 0;
- var fileOptions = FileOptions.SequentialScan;
- _allowAsyncFileRead = false;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (AsyncFile.UseAsyncIO)
- {
- fileOptions |= FileOptions.Asynchronous;
- _allowAsyncFileRead = true;
- }
+ _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
+ }
- _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
+ /// </summary>
+ /// <param name="stream">The stream to progressively copy.</param>
+ /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
+ public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
+ {
+ _job = null;
+ _transcodingJobHelper = null;
+ _timeoutMs = timeoutMs;
+ _stream = stream;
}
/// <inheritdoc />
- public override bool CanRead => _fileStream.CanRead;
+ public override bool CanRead => _stream.CanRead;
/// <inheritdoc />
public override bool CanSeek => false;
@@ -70,13 +71,13 @@ namespace Jellyfin.Api.Helpers
/// <inheritdoc />
public override void Flush()
{
- _fileStream.Flush();
+ _stream.Flush();
}
/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
{
- return _fileStream.Read(buffer, offset, count);
+ return _stream.Read(buffer, offset, count);
}
/// <inheritdoc />
@@ -90,15 +91,7 @@ namespace Jellyfin.Api.Helpers
while (remainingBytesToRead > 0)
{
cancellationToken.ThrowIfCancellationRequested();
- int bytesRead;
- if (_allowAsyncFileRead)
- {
- bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead);
- }
+ int bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
remainingBytesToRead -= bytesRead;
newOffset += bytesRead;
@@ -152,11 +145,11 @@ namespace Jellyfin.Api.Helpers
{
if (disposing)
{
- _fileStream.Dispose();
+ _stream.Dispose();
if (_job != null)
{
- _transcodingJobHelper.OnTranscodeEndRequest(_job);
+ _transcodingJobHelper?.OnTranscodeEndRequest(_job);
}
}
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 0041251e3..4fc791665 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
@@ -81,7 +82,7 @@ namespace Jellyfin.Api.Helpers
throw new ResourceNotFoundException(nameof(httpRequest.Path));
}
- var url = httpRequest.Path.Value.Split('.')[^1];
+ var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
{
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 4e1e98df0..07d0b5543 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
@@ -86,8 +87,8 @@ namespace Jellyfin.Api.Helpers
DeleteEncodedMediaCache();
- sessionManager!.PlaybackProgress += OnPlaybackProgress;
- sessionManager!.PlaybackStart += OnPlaybackProgress;
+ sessionManager.PlaybackProgress += OnPlaybackProgress;
+ sessionManager.PlaybackStart += OnPlaybackProgress;
}
/// <summary>
@@ -557,7 +558,7 @@ namespace Jellyfin.Api.Helpers
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
// FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
@@ -607,6 +608,10 @@ namespace Jellyfin.Api.Helpers
{
StartThrottler(state, transcodingJob);
}
+ else if (transcodingJob.ExitCode != 0)
+ {
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
+ }
_logger.LogDebug("StartFfMpeg() finished successfully");
@@ -743,6 +748,7 @@ namespace Jellyfin.Api.Helpers
private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
{
job.HasExited = true;
+ job.ExitCode = process.ExitCode;
_logger.LogDebug("Disposing stream resources");
state.Dispose();
@@ -878,8 +884,8 @@ namespace Jellyfin.Api.Helpers
if (disposing)
{
_loggerFactory.Dispose();
- _sessionManager!.PlaybackProgress -= OnPlaybackProgress;
- _sessionManager!.PlaybackStart -= OnPlaybackProgress;
+ _sessionManager.PlaybackProgress -= OnPlaybackProgress;
+ _sessionManager.PlaybackStart -= OnPlaybackProgress;
}
}
}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 2fca88f24..57480b2f3 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -6,18 +6,17 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
- <AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.9" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" />
- <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
+ <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
index be2045fba..d2e78ac88 100644
--- a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
@@ -32,7 +32,8 @@ namespace Jellyfin.Api.ModelBinders
{
try
{
- var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue);
+ // REVIEW: This shouldn't be null here
+ var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue!);
bindingContext.Result = ModelBindingResult.Success(convertedValue);
}
catch (FormatException e)
@@ -44,4 +45,4 @@ namespace Jellyfin.Api.ModelBinders
return Task.CompletedTask;
}
}
-} \ No newline at end of file
+}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index 291e571dc..fed837b85 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -107,6 +107,11 @@ namespace Jellyfin.Api.Models.PlaybackDtos
public bool HasExited { get; set; }
/// <summary>
+ /// Gets or sets exit code.
+ /// </summary>
+ public int ExitCode { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether is user paused.
/// </summary>
public bool IsUserPaused { get; set; }
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index e95f2d1f4..cbabf087b 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -55,11 +55,14 @@ namespace Jellyfin.Api.Models.StreamingDtos
/// <summary>
/// Gets the video request.
/// </summary>
- public VideoRequestDto? VideoRequest => Request! as VideoRequestDto;
+ public VideoRequestDto? VideoRequest => Request as VideoRequestDto;
/// <summary>
/// Gets or sets the direct stream provicer.
/// </summary>
+ /// <remarks>
+ /// Deprecated.
+ /// </remarks>
public IDirectStreamProvider? DirectStreamProvider { get; set; }
/// <summary>
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 65bbd49da..2de53e7c8 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -35,7 +35,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0-rc.2*" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 8cee5dcae..5fa386eca 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -28,11 +28,6 @@
<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="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
index faf814c06..61db223d9 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
@@ -226,5 +226,10 @@ namespace Jellyfin.Networking.Configuration
/// Gets or sets the known proxies. If the proxy is a network, it's added to the KnownNetworks.
/// </summary>
public string[] KnownProxies { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests.
+ /// </summary>
+ public bool EnablePublishedServerUriByRequest { get; set; } = false;
}
}
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index 227a41ce4..0cd9a5915 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index a75b28593..e26cf093b 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -19,13 +19,13 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.9" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.9">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0-rc.2*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.9">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0-rc.2*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 244abf469..3ab043c64 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -2,12 +2,12 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Net;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Server.Implementations.Security
@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations.Security
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached != null)
{
- return Task.FromResult((AuthorizationInfo)cached!); // Cache should never contain null
+ return Task.FromResult((AuthorizationInfo)cached); // Cache should never contain null
}
return GetAuthorization(requestContext);
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index 6e98ad863..5e84255f9 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -93,13 +93,9 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc />
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
{
- string pin;
- using (var cryptoRandom = RandomNumberGenerator.Create())
- {
- byte[] bytes = new byte[4];
- cryptoRandom.GetBytes(bytes);
- pin = BitConverter.ToString(bytes);
- }
+ byte[] bytes = new byte[4];
+ RandomNumberGenerator.Fill(bytes);
+ string pin = BitConverter.ToString(bytes);
DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
string filePath = _passwordResetFileBase + user.Id + ".json";
@@ -114,7 +110,6 @@ namespace Jellyfin.Server.Implementations.Users
await using (FileStream fileStream = AsyncFile.OpenWrite(filePath))
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
- await fileStream.FlushAsync().ConfigureAwait(false);
}
user.EasyPassword = pin;
@@ -123,6 +118,7 @@ namespace Jellyfin.Server.Implementations.Users
{
Action = ForgotPasswordAction.PinCode,
PinExpirationDate = expireTime,
+ PinFile = filePath
};
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 02377bfd7..704a6a84e 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -701,6 +701,11 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task ClearProfileImageAsync(User user)
{
+ if (user.ProfileImage == null)
+ {
+ return;
+ }
+
await using var dbContext = _dbProvider.CreateContext();
dbContext.Remove(user.ProfileImage);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
diff --git a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
index 0d04b6bb1..b061be33b 100644
--- a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
+++ b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Server.Configuration
}
/// <inheritdoc />
- public Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName)
+ public Task<CorsPolicy?> GetPolicyAsync(HttpContext context, string? policyName)
{
var corsHosts = _serverConfigurationManager.Configuration.CorsHosts;
var builder = new CorsPolicyBuilder()
@@ -43,7 +43,7 @@ namespace Jellyfin.Server.Configuration
.AllowCredentials();
}
- return Task.FromResult(builder.Build());
+ return Task.FromResult<CorsPolicy?>(builder.Build());
}
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index f19e87aba..867f79b6b 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -188,7 +188,8 @@ namespace Jellyfin.Server.Extensions
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
- options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+ options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
+
if (config.KnownProxies.Length == 0)
{
options.KnownNetworks.Clear();
@@ -278,7 +279,7 @@ namespace Jellyfin.Server.Extensions
{
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
- Name = "X-Emby-Authorization",
+ Name = "Authorization",
Description = "API key header parameter"
});
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
new file mode 100644
index 000000000..73a619b8d
--- /dev/null
+++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
@@ -0,0 +1,144 @@
+// The MIT License (MIT)
+//
+// Copyright (c) .NET Foundation and Contributors
+//
+// All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Server.Infrastructure
+{
+ /// <inheritdoc />
+ public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
+ /// </summary>
+ /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override FileMetadata GetFileInfo(string path)
+ {
+ var fileInfo = new FileInfo(path);
+ var length = fileInfo.Length;
+ // This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
+ if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
+ {
+ using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ length = RandomAccess.GetLength(fileHandle);
+ }
+
+ return new FileMetadata
+ {
+ Exists = fileInfo.Exists,
+ Length = length,
+ LastModified = fileInfo.LastWriteTimeUtc
+ };
+ }
+
+ /// <inheritdoc />
+ protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (result == null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (range != null && rangeLength == 0)
+ {
+ return Task.CompletedTask;
+ }
+
+ // It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
+ if (!IsSymLink(result.FileName))
+ {
+ return base.WriteFileAsync(context, result, range, rangeLength);
+ }
+
+ var response = context.HttpContext.Response;
+
+ if (range != null)
+ {
+ return SendFileAsync(
+ result.FileName,
+ response,
+ offset: range.From ?? 0L,
+ count: rangeLength);
+ }
+
+ return SendFileAsync(
+ result.FileName,
+ response,
+ offset: 0,
+ count: null);
+ }
+
+ private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
+ {
+ var fileInfo = GetFileInfo(filePath);
+ if (offset < 0 || offset > fileInfo.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
+ }
+
+ if (count.HasValue
+ && (count.Value < 0 || count.Value > fileInfo.Length - offset))
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
+ }
+
+ // Copied from SendFileFallback.SendFileAsync
+ const int BufferSize = 1024 * 16;
+
+ await using var fileStream = new FileStream(
+ filePath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite,
+ bufferSize: BufferSize,
+ options: FileOptions.Asynchronous | FileOptions.SequentialScan);
+
+ fileStream.Seek(offset, SeekOrigin.Begin);
+ await StreamCopyOperation
+ .CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
+ .ConfigureAwait(true);
+ }
+
+ private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 1fdad73b7..8983eb50f 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -8,7 +8,7 @@
<PropertyGroup>
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<ServerGarbageCollection>false</ServerGarbageCollection>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -31,15 +31,15 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.9" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.9" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.0-rc.2*" />
<PackageReference Include="prometheus-net" Version="5.0.1" />
<PackageReference Include="prometheus-net.AspNetCore" Version="5.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
- <PackageReference Include="Serilog.Settings.Configuration" Version="3.2.0" />
+ <PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
index fd0ebbf43..cdd86e28e 100644
--- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
+++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
@@ -27,7 +27,11 @@ namespace Jellyfin.Server.Middleware
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
- httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>()));
+ var feature = httpContext.Features.Get<IQueryFeature>();
+ if (feature != null)
+ {
+ httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature));
+ }
await _next(httpContext).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
index c1f5b5dfa..e4d2937e7 100644
--- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
+++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Web;
using Jellyfin.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
@@ -52,20 +51,14 @@ namespace Jellyfin.Server.Middleware
return;
}
- // Unencode and re-parse querystring.
- var unencodedKey = HttpUtility.UrlDecode(key);
-
- if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
+ if (!key.Contains('='))
{
- // Don't do anything if it's not encoded.
_store = value;
return;
}
var pairs = new Dictionary<string, StringValues>();
- var queryString = unencodedKey.SpanSplit('&');
-
- foreach (var pair in queryString)
+ foreach (var pair in key.SpanSplit('&'))
{
var i = pair.IndexOf('=');
if (i == -1)
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 6ff59626d..40f871759 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -120,7 +120,7 @@ namespace Jellyfin.Server.Migrations.Routines
var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client)
{
- IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
+ IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : null,
ShowBackdrop = dto.ShowBackdrop,
ShowSidebar = dto.ShowSidebar,
ScrollDirection = dto.ScrollDirection,
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index d9524645a..9b2d603c7 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -1,7 +1,6 @@
using System;
using System.IO;
using Emby.Server.Implementations.Data;
-using Emby.Server.Implementations.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
@@ -10,6 +9,7 @@ using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger<MigrateUserDb> _logger;
private readonly IServerApplicationPaths _paths;
private readonly JellyfinDbProvider _provider;
- private readonly MyXmlSerializer _xmlSerializer;
+ private readonly IXmlSerializer _xmlSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
ILogger<MigrateUserDb> logger,
IServerApplicationPaths paths,
JellyfinDbProvider provider,
- MyXmlSerializer xmlSerializer)
+ IXmlSerializer xmlSerializer)
{
_logger = logger;
_paths = paths;
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 3c0ee069d..45699f3af 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -195,9 +195,9 @@ namespace Jellyfin.Server
try
{
- await webHost.StartAsync().ConfigureAwait(false);
+ await webHost.StartAsync(_tokenSource.Token).ConfigureAwait(false);
}
- catch
+ catch (Exception ex) when (ex is not TaskCanceledException)
{
_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;
@@ -547,7 +547,7 @@ namespace Jellyfin.Server
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
// Copy the resource contents to the expected file path for the config file
- await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await resource.CopyToAsync(dst).ConfigureAwait(false);
}
@@ -594,7 +594,7 @@ namespace Jellyfin.Server
try
{
// Serilog.Log is used by SerilogLoggerFactory when no logger is specified
- Serilog.Log.Logger = new LoggerConfiguration()
+ Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
@@ -602,7 +602,7 @@ namespace Jellyfin.Server
}
catch (Exception ex)
{
- Serilog.Log.Logger = new LoggerConfiguration()
+ Log.Logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
@@ -613,7 +613,7 @@ namespace Jellyfin.Server
.Enrich.WithThreadId()
.CreateLogger();
- Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
+ Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 60cdc2f6f..8085c2630 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -7,6 +7,7 @@ using System.Text;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Infrastructure;
using Jellyfin.Server.Middleware;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -14,6 +15,8 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -56,6 +59,9 @@ namespace Jellyfin.Server
{
options.HttpsPort = _serverApplicationHost.HttpsPort;
});
+
+ // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
+ services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinApiSwagger();
diff --git a/MediaBrowser.MediaEncoding/FfmpegException.cs b/MediaBrowser.Common/FfmpegException.cs
index 1697fd33a..be420196d 100644
--- a/MediaBrowser.MediaEncoding/FfmpegException.cs
+++ b/MediaBrowser.Common/FfmpegException.cs
@@ -1,6 +1,6 @@
using System;
-namespace MediaBrowser.MediaEncoding
+namespace MediaBrowser.Common
{
/// <summary>
/// Represents errors that occur during interaction with FFmpeg.
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 12cfaf978..9c8ce4ac5 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -19,8 +19,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
@@ -29,7 +29,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
index a64919700..cc0a107a8 100644
--- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs
+++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
@@ -74,6 +74,6 @@ namespace MediaBrowser.Controller.Dlna
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>DlnaIconResponse.</returns>
- ImageStream GetIcon(string filename);
+ ImageStream? GetIcon(string filename);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index c7f61a90b..7ca0e851b 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -58,7 +58,7 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>Guid.</returns>
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
- string GetImageCacheTag(BaseItem item, ChapterInfo info);
+ string GetImageCacheTag(BaseItem item, ChapterInfo chapter);
string? GetImageCacheTag(User user);
diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs
index 5d552170f..f4c305799 100644
--- a/MediaBrowser.Controller/Drawing/ImageStream.cs
+++ b/MediaBrowser.Controller/Drawing/ImageStream.cs
@@ -8,11 +8,16 @@ namespace MediaBrowser.Controller.Drawing
{
public class ImageStream : IDisposable
{
+ public ImageStream(Stream stream)
+ {
+ Stream = stream;
+ }
+
/// <summary>
- /// Gets or sets the stream.
+ /// Gets the stream.
/// </summary>
/// <value>The stream.</value>
- public Stream? Stream { get; set; }
+ public Stream Stream { get; }
/// <summary>
/// Gets or sets the format.
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index f4c91973b..838a9f2f8 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -44,18 +44,10 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// The trailer folder name.
/// </summary>
- public const string TrailerFolderName = "trailers";
+ public const string TrailersFolderName = "trailers";
public const string ThemeSongsFolderName = "theme-music";
- public const string ThemeSongFilename = "theme";
+ public const string ThemeSongFileName = "theme";
public const string ThemeVideosFolderName = "backdrops";
- public const string ExtrasFolderName = "extras";
- public const string BehindTheScenesFolderName = "behind the scenes";
- public const string DeletedScenesFolderName = "deleted scenes";
- public const string InterviewFolderName = "interviews";
- public const string SceneFolderName = "scenes";
- public const string SampleFolderName = "samples";
- public const string ShortsFolderName = "shorts";
- public const string FeaturettesFolderName = "featurettes";
/// <summary>
/// The supported image extensions.
@@ -93,16 +85,20 @@ namespace MediaBrowser.Controller.Entities
};
public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
- public static readonly string[] AllExtrasTypesFolderNames =
- {
- ExtrasFolderName,
- BehindTheScenesFolderName,
- DeletedScenesFolderName,
- InterviewFolderName,
- SceneFolderName,
- SampleFolderName,
- ShortsFolderName,
- FeaturettesFolderName
+
+ /// <summary>
+ /// The supported extra folder names and types. See <see cref="Emby.Naming.Common.NamingOptions" />.
+ /// </summary>
+ public static readonly Dictionary<string, ExtraType> AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
+ {
+ ["extras"] = MediaBrowser.Model.Entities.ExtraType.Unknown,
+ ["behind the scenes"] = MediaBrowser.Model.Entities.ExtraType.BehindTheScenes,
+ ["deleted scenes"] = MediaBrowser.Model.Entities.ExtraType.DeletedScene,
+ ["interviews"] = MediaBrowser.Model.Entities.ExtraType.Interview,
+ ["scenes"] = MediaBrowser.Model.Entities.ExtraType.Scene,
+ ["samples"] = MediaBrowser.Model.Entities.ExtraType.Sample,
+ ["shorts"] = MediaBrowser.Model.Entities.ExtraType.Clip,
+ ["featurettes"] = MediaBrowser.Model.Entities.ExtraType.Clip
};
private string _sortName;
@@ -1358,7 +1354,7 @@ namespace MediaBrowser.Controller.Entities
// Support plex/xbmc convention
files.AddRange(fileSystemChildren
- .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
+ .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFileName, StringComparison.OrdinalIgnoreCase)));
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
.OfType<Audio.Audio>()
@@ -1417,39 +1413,24 @@ namespace MediaBrowser.Controller.Entities
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
- var extras = new List<Video>();
-
- var libraryOptions = new LibraryOptions();
- var folders = fileSystemChildren.Where(i => i.IsDirectory).ToList();
- foreach (var extraFolderName in AllExtrasTypesFolderNames)
- {
- var files = folders
- .Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => FileSystem.GetFiles(i.FullName));
-
- // Re-using the same instance of LibraryOptions since it looks like it's never being altered.
- extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, libraryOptions)
+ return fileSystemChildren
+ .Where(child => child.IsDirectory && AllExtrasTypesFolderNames.ContainsKey(child.Name))
+ .SelectMany(folder => LibraryManager
+ .ResolvePaths(FileSystem.GetFiles(folder.FullName), directoryService, null, new LibraryOptions())
.OfType<Video>()
- .Select(item =>
+ .Select(video =>
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
- if (LibraryManager.GetItemById(item.Id) is Video dbItem)
+ if (LibraryManager.GetItemById(video.Id) is Video dbItem)
{
- item = dbItem;
+ video = dbItem;
}
- // Use some hackery to get the extra type based on foldername
- item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty, StringComparison.Ordinal), true, out ExtraType extraType)
- ? extraType
- : Model.Entities.ExtraType.Unknown;
-
- return item;
-
- // Sort them so that the list can be easily compared for changes
- }).OrderBy(i => i.Path));
- }
-
- return extras.ToArray();
+ video.ExtraType = AllExtrasTypesFolderNames[folder.Name];
+ return video;
+ })
+ .OrderBy(video => video.Path)) // Sort them so that the list can be easily compared for changes
+ .ToArray();
}
public Task RefreshMetadata(CancellationToken cancellationToken)
@@ -3016,7 +2997,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- public bool Equals(BaseItem other) => object.Equals(Id, other?.Id);
+ public bool Equals(BaseItem other) => Equals(Id, other?.Id);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(Id);
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index e88121212..33870e2fb 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="file">The file.</param>
public static void SetImagePath(this BaseItem item, ImageType imageType, string file)
{
- if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase))
+ if (file.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
item.SetImage(
new ItemImageInfo
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
index 0853200dd..afdaf448b 100644
--- a/MediaBrowser.Controller/Entities/Year.cs
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Entities
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
- var usCulture = new CultureInfo("en-US");
-
- if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out var year))
+ if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
return new List<BaseItem>();
}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 3da0a5875..7e5e742f5 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -50,11 +50,11 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the system info.
/// </summary>
- /// <param name="source">The originator of the request.</param>
+ /// <param name="request">The HTTP request.</param>
/// <returns>SystemInfo.</returns>
- SystemInfo GetSystemInfo(IPAddress source);
+ SystemInfo GetSystemInfo(HttpRequest request);
- PublicSystemInfo GetPublicSystemInfo(IPAddress address);
+ PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
/// <summary>
/// Gets a URL specific for the request.
diff --git a/MediaBrowser.Controller/Library/IDirectStreamProvider.cs b/MediaBrowser.Controller/Library/IDirectStreamProvider.cs
new file mode 100644
index 000000000..96f8b7eba
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IDirectStreamProvider.cs
@@ -0,0 +1,19 @@
+using System.IO;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// The direct live TV stream provider.
+ /// </summary>
+ /// <remarks>
+ /// Deprecated.
+ /// </remarks>
+ public interface IDirectStreamProvider
+ {
+ /// <summary>
+ /// Gets the live stream, shared streams seek to the end of the file first.
+ /// </summary>
+ /// <returns>The stream.</returns>
+ Stream GetStream();
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs
index 323aa4876..4c44a17fd 100644
--- a/MediaBrowser.Controller/Library/ILiveStream.cs
+++ b/MediaBrowser.Controller/Library/ILiveStream.cs
@@ -2,6 +2,7 @@
#pragma warning disable CA1711, CS1591
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Dto;
@@ -25,5 +26,7 @@ namespace MediaBrowser.Controller.Library
Task Open(CancellationToken openCancellationToken);
Task Close();
+
+ Stream GetStream();
}
}
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index fd3631da9..e802796d3 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@@ -111,6 +110,20 @@ namespace MediaBrowser.Controller.Library
Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken);
/// <summary>
+ /// Gets the live stream info.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>An instance of <see cref="ILiveStream"/>.</returns>
+ public ILiveStream GetLiveStreamInfo(string id);
+
+ /// <summary>
+ /// Gets the live stream info using the stream's unique id.
+ /// </summary>
+ /// <param name="uniqueId">The unique identifier.</param>
+ /// <returns>An instance of <see cref="ILiveStream"/>.</returns>
+ public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId);
+
+ /// <summary>
/// Closes the media source.
/// </summary>
/// <param name="id">The live stream identifier.</param>
@@ -126,14 +139,5 @@ namespace MediaBrowser.Controller.Library
void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user);
Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken);
-
- Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken);
- }
-
- public interface IDirectStreamProvider
- {
- Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
-
- string GetFilePath();
}
}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 0f697bccc..d37880865 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -14,11 +14,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Diacritics" Version="2.1.20036.1" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
+ <PackageReference Include="Diacritics" Version="3.3.4" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
- <PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0" />
+ <PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0-rc.2*" />
</ItemGroup>
<ItemGroup>
@@ -32,18 +32,13 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
- <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
- </PropertyGroup>
-
- <PropertyGroup Condition=" '$(Configuration)' == 'Release'">
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index bdb379332..5715194b8 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -21,8 +21,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{
public class EncodingHelper
{
- private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder;
@@ -816,7 +814,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
- if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel))
+ if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel))
{
if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
@@ -911,7 +909,7 @@ namespace MediaBrowser.Controller.MediaEncoding
CultureInfo.InvariantCulture,
"subtitles='{0}:si={1}'{2}",
_mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
- state.InternalSubtitleStreamOffset.ToString(_usCulture),
+ state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture),
// fallbackFontParam,
setPtsParam);
}
@@ -1217,7 +1215,7 @@ namespace MediaBrowser.Controller.MediaEncoding
param += string.Format(
CultureInfo.InvariantCulture,
" -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
- profileScore.ToString(_usCulture),
+ profileScore.ToString(CultureInfo.InvariantCulture),
crf,
qmin,
qmax);
@@ -1289,7 +1287,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var framerate = GetFramerateParam(state);
if (framerate.HasValue)
{
- param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(_usCulture));
+ param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(CultureInfo.InvariantCulture));
}
var targetVideoCodec = state.ActualOutputVideoCodec;
@@ -1393,7 +1391,7 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
{
// hevc_qsv use -level 51 instead of -level 153.
- if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
+ if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double hevcLevel))
{
param += " -level " + (hevcLevel / 3);
}
@@ -1555,7 +1553,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a specific level was requested, the source must match or be less than
var level = state.GetRequestedLevel(videoStream.Codec);
if (!string.IsNullOrEmpty(level)
- && double.TryParse(level, NumberStyles.Any, _usCulture, out var requestLevel))
+ && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel))
{
if (!videoStream.Level.HasValue)
{
@@ -1803,7 +1801,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.AudioStream.Channels.Value > 5
&& !encodingOptions.DownMixAudioBoost.Equals(1))
{
- filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture));
+ filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
}
var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive;
@@ -2434,8 +2432,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isExynosV4L2)
{
- var widthParam = requestedWidth.Value.ToString(_usCulture);
- var heightParam = requestedHeight.Value.ToString(_usCulture);
+ var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture);
+ var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture);
filters.Add(
string.Format(
@@ -2453,8 +2451,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue)
{
- var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture);
- var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture);
+ var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture);
+ var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture);
if (isExynosV4L2)
{
@@ -2486,7 +2484,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else
{
- var widthParam = requestedWidth.Value.ToString(_usCulture);
+ var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture);
filters.Add(
string.Format(
@@ -2499,7 +2497,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a fixed height was requested
else if (requestedHeight.HasValue)
{
- var heightParam = requestedHeight.Value.ToString(_usCulture);
+ var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture);
if (isExynosV4L2)
{
@@ -2522,7 +2520,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a max width was requested
else if (requestedMaxWidth.HasValue)
{
- var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture);
+ var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture);
if (isExynosV4L2)
{
@@ -2545,7 +2543,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a max height was requested
else if (requestedMaxHeight.HasValue)
{
- var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture);
+ var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture);
if (isExynosV4L2)
{
@@ -4122,12 +4120,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (bitrate.HasValue)
{
- args += " -ab " + bitrate.Value.ToString(_usCulture);
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
- args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
+ args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
args += GetAudioFilterParam(state, encodingOptions);
@@ -4143,12 +4141,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (bitrate.HasValue)
{
- audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture));
+ audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
}
if (state.OutputAudioChannels.HasValue)
{
- audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(_usCulture));
+ audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
}
// opus will fail on 44100
@@ -4156,7 +4154,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (state.OutputAudioSampleRate.HasValue)
{
- audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture));
+ audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index b09b7dba6..e92c4a08a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -541,7 +541,12 @@ namespace MediaBrowser.Controller.MediaEncoding
return MimeType;
}
- return MimeTypes.GetMimeType(outputPath, enableStreamDefault);
+ if (enableStreamDefault)
+ {
+ return MimeTypes.GetMimeType(outputPath);
+ }
+
+ return MimeTypes.GetMimeType(outputPath, null);
}
public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index c5522bc3c..e6511ca8d 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -95,9 +95,10 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="mediaSource">Media source information.</param>
/// <param name="imageStream">Media stream information.</param>
/// <param name="imageStreamIndex">Index of the stream to extract from.</param>
+ /// <param name="outputExtension">The extension of the file to write, including the '.'.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
/// <returns>Location of video image.</returns>
- Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken);
+ Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken);
/// <summary>
/// Extracts the video images on interval.
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index aa5e2c403..933f440ac 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -13,7 +13,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{
public class JobLogger
{
- private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private readonly ILogger _logger;
public JobLogger(ILogger logger)
@@ -87,7 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var rate = parts[i + 1];
- if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
+ if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
{
framerate = val;
}
@@ -96,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var rate = part.Split('=', 2)[^1];
- if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
+ if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
{
framerate = val;
}
@@ -106,7 +105,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var time = part.Split('=', 2)[^1];
- if (TimeSpan.TryParse(time, _usCulture, out var val))
+ if (TimeSpan.TryParse(time, CultureInfo.InvariantCulture, out var val))
{
var currentMs = startMs + val.TotalMilliseconds;
@@ -120,7 +119,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var size = part.Split('=', 2)[^1];
int? scale = null;
- if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
+ if (size.Contains("kb", StringComparison.OrdinalIgnoreCase))
{
scale = 1024;
size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
@@ -128,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (scale.HasValue)
{
- if (long.TryParse(size, NumberStyles.Any, _usCulture, out var val))
+ if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
{
bytesTranscoded = val * scale.Value;
}
@@ -139,7 +138,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var rate = part.Split('=', 2)[^1];
int? scale = null;
- if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
+ if (rate.Contains("kbits/s", StringComparison.OrdinalIgnoreCase))
{
scale = 1024;
rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
@@ -147,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (scale.HasValue)
{
- if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
+ if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
{
bitRate = (int)Math.Ceiling(val * scale.Value);
}
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
index 3330dd540..52aa44024 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -31,12 +31,14 @@ namespace MediaBrowser.Controller.Subtitles
/// <param name="video">The video.</param>
/// <param name="language">Subtitle language.</param>
/// <param name="isPerfectMatch">Require perfect match.</param>
+ /// <param name="isAutomated">Request is automated.</param>
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
/// <returns>Subtitles, wrapped in task.</returns>
Task<RemoteSubtitleInfo[]> SearchSubtitles(
Video video,
string language,
bool? isPerfectMatch,
+ bool isAutomated,
CancellationToken cancellationToken);
/// <summary>
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
index 767d87d46..ef052237a 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
@@ -51,5 +51,7 @@ namespace MediaBrowser.Controller.Subtitles
public string[] DisabledSubtitleFetchers { get; set; }
public string[] SubtitleFetcherOrder { get; set; }
+
+ public bool IsAutomated { get; set; }
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
index b8ae9f3ff..f49876cca 100644
--- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.SyncPlay.Queue
@@ -19,10 +20,16 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
private const int NoPlayingItemIndex = -1;
/// <summary>
- /// Random number generator used to shuffle lists.
+ /// The sorted playlist.
/// </summary>
- /// <value>The random number generator.</value>
- private readonly Random _randomNumberGenerator = new Random();
+ /// <value>The sorted playlist, or play queue of the group.</value>
+ private List<QueueItem> _sortedPlaylist = new List<QueueItem>();
+
+ /// <summary>
+ /// The shuffled playlist.
+ /// </summary>
+ /// <value>The shuffled playlist, or play queue of the group.</value>
+ private List<QueueItem> _shuffledPlaylist = new List<QueueItem>();
/// <summary>
/// Initializes a new instance of the <see cref="PlayQueueManager" /> class.
@@ -57,18 +64,6 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
public GroupRepeatMode RepeatMode { get; private set; } = GroupRepeatMode.RepeatNone;
/// <summary>
- /// Gets or sets the sorted playlist.
- /// </summary>
- /// <value>The sorted playlist, or play queue of the group.</value>
- private List<QueueItem> SortedPlaylist { get; set; } = new List<QueueItem>();
-
- /// <summary>
- /// Gets or sets the shuffled playlist.
- /// </summary>
- /// <value>The shuffled playlist, or play queue of the group.</value>
- private List<QueueItem> ShuffledPlaylist { get; set; } = new List<QueueItem>();
-
- /// <summary>
/// Checks if an item is playing.
/// </summary>
/// <returns><c>true</c> if an item is playing; <c>false</c> otherwise.</returns>
@@ -92,14 +87,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// <param name="items">The new items of the playlist.</param>
public void SetPlaylist(IReadOnlyList<Guid> items)
{
- SortedPlaylist.Clear();
- ShuffledPlaylist.Clear();
+ _sortedPlaylist.Clear();
+ _shuffledPlaylist.Clear();
- SortedPlaylist = CreateQueueItemsFromArray(items);
+ _sortedPlaylist = CreateQueueItemsFromArray(items);
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
- Shuffle(ShuffledPlaylist);
+ _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist.Shuffle();
}
PlayingItemIndex = NoPlayingItemIndex;
@@ -114,10 +109,10 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
var newItems = CreateQueueItemsFromArray(items);
- SortedPlaylist.AddRange(newItems);
+ _sortedPlaylist.AddRange(newItems);
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- ShuffledPlaylist.AddRange(newItems);
+ _shuffledPlaylist.AddRange(newItems);
}
LastChange = DateTime.UtcNow;
@@ -130,26 +125,26 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (PlayingItemIndex == NoPlayingItemIndex)
{
- ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
- Shuffle(ShuffledPlaylist);
+ _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist.Shuffle();
}
else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
{
// First time shuffle.
- var playingItem = SortedPlaylist[PlayingItemIndex];
- ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
- ShuffledPlaylist.RemoveAt(PlayingItemIndex);
- Shuffle(ShuffledPlaylist);
- ShuffledPlaylist.Insert(0, playingItem);
+ var playingItem = _sortedPlaylist[PlayingItemIndex];
+ _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist.RemoveAt(PlayingItemIndex);
+ _shuffledPlaylist.Shuffle();
+ _shuffledPlaylist.Insert(0, playingItem);
PlayingItemIndex = 0;
}
else
{
// Re-shuffle playlist.
- var playingItem = ShuffledPlaylist[PlayingItemIndex];
- ShuffledPlaylist.RemoveAt(PlayingItemIndex);
- Shuffle(ShuffledPlaylist);
- ShuffledPlaylist.Insert(0, playingItem);
+ var playingItem = _shuffledPlaylist[PlayingItemIndex];
+ _shuffledPlaylist.RemoveAt(PlayingItemIndex);
+ _shuffledPlaylist.Shuffle();
+ _shuffledPlaylist.Insert(0, playingItem);
PlayingItemIndex = 0;
}
@@ -164,11 +159,11 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (PlayingItemIndex != NoPlayingItemIndex)
{
- var playingItem = ShuffledPlaylist[PlayingItemIndex];
- PlayingItemIndex = SortedPlaylist.IndexOf(playingItem);
+ var playingItem = _shuffledPlaylist[PlayingItemIndex];
+ PlayingItemIndex = _sortedPlaylist.IndexOf(playingItem);
}
- ShuffledPlaylist.Clear();
+ _shuffledPlaylist.Clear();
ShuffleMode = GroupShuffleMode.Sorted;
LastChange = DateTime.UtcNow;
@@ -181,16 +176,16 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
public void ClearPlaylist(bool clearPlayingItem)
{
var playingItem = GetPlayingItem();
- SortedPlaylist.Clear();
- ShuffledPlaylist.Clear();
+ _sortedPlaylist.Clear();
+ _shuffledPlaylist.Clear();
LastChange = DateTime.UtcNow;
if (!clearPlayingItem && playingItem != null)
{
- SortedPlaylist.Add(playingItem);
+ _sortedPlaylist.Add(playingItem);
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- ShuffledPlaylist.Add(playingItem);
+ _shuffledPlaylist.Add(playingItem);
}
PlayingItemIndex = 0;
@@ -212,14 +207,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
var playingItem = GetPlayingItem();
- var sortedPlayingItemIndex = SortedPlaylist.IndexOf(playingItem);
+ var sortedPlayingItemIndex = _sortedPlaylist.IndexOf(playingItem);
// Append items to sorted and shuffled playlist as they are.
- SortedPlaylist.InsertRange(sortedPlayingItemIndex + 1, newItems);
- ShuffledPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
+ _sortedPlaylist.InsertRange(sortedPlayingItemIndex + 1, newItems);
+ _shuffledPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
}
else
{
- SortedPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
+ _sortedPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
}
LastChange = DateTime.UtcNow;
@@ -298,8 +293,8 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
var playingItem = GetPlayingItem();
- SortedPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
- ShuffledPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
+ _sortedPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
+ _shuffledPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
LastChange = DateTime.UtcNow;
@@ -313,7 +308,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
// Was first element, picking next if available.
// Default to no playing item otherwise.
- PlayingItemIndex = SortedPlaylist.Count > 0 ? 0 : NoPlayingItemIndex;
+ PlayingItemIndex = _sortedPlaylist.Count > 0 ? 0 : NoPlayingItemIndex;
}
return true;
@@ -363,8 +358,8 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// </summary>
public void Reset()
{
- SortedPlaylist.Clear();
- ShuffledPlaylist.Clear();
+ _sortedPlaylist.Clear();
+ _shuffledPlaylist.Clear();
PlayingItemIndex = NoPlayingItemIndex;
ShuffleMode = GroupShuffleMode.Sorted;
RepeatMode = GroupRepeatMode.RepeatNone;
@@ -460,7 +455,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
}
PlayingItemIndex++;
- if (PlayingItemIndex >= SortedPlaylist.Count)
+ if (PlayingItemIndex >= _sortedPlaylist.Count)
{
if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
{
@@ -468,7 +463,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
}
else
{
- PlayingItemIndex = SortedPlaylist.Count - 1;
+ PlayingItemIndex = _sortedPlaylist.Count - 1;
return false;
}
}
@@ -494,7 +489,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
{
- PlayingItemIndex = SortedPlaylist.Count - 1;
+ PlayingItemIndex = _sortedPlaylist.Count - 1;
}
else
{
@@ -508,23 +503,6 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
}
/// <summary>
- /// Shuffles a given list.
- /// </summary>
- /// <param name="list">The list to shuffle.</param>
- private void Shuffle<T>(IList<T> list)
- {
- int n = list.Count;
- while (n > 1)
- {
- n--;
- int k = _randomNumberGenerator.Next(n + 1);
- T value = list[k];
- list[k] = list[n];
- list[n] = value;
- }
- }
-
- /// <summary>
/// Creates a list from the array of items. Each item is given an unique playlist identifier.
/// </summary>
/// <returns>The list of queue items.</returns>
@@ -548,11 +526,11 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- return ShuffledPlaylist;
+ return _shuffledPlaylist;
}
else
{
- return SortedPlaylist;
+ return _sortedPlaylist;
}
}
@@ -568,11 +546,11 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
}
else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- return ShuffledPlaylist[PlayingItemIndex];
+ return _shuffledPlaylist[PlayingItemIndex];
}
else
{
- return SortedPlaylist[PlayingItemIndex];
+ return _sortedPlaylist[PlayingItemIndex];
}
}
}
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index b7398880e..988581df9 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -60,8 +60,6 @@ namespace MediaBrowser.LocalMetadata.Images
private readonly IFileSystem _fileSystem;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
/// <summary>
/// Initializes a new instance of the <see cref="LocalImageProvider"/> class.
/// </summary>
@@ -434,7 +432,7 @@ namespace MediaBrowser.LocalMetadata.Images
var seasonMarker = seasonNumber.Value == 0
? "-specials"
- : seasonNumber.Value.ToString("00", _usCulture);
+ : seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture);
// Get this one directly from the file system since we have to go up a level
if (!string.Equals(prefix, seasonMarker, StringComparison.OrdinalIgnoreCase))
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 1cf8fcd1b..a3db717b9 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -11,7 +11,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index ef130ee74..5a36c1663 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -20,8 +21,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
public class BaseItemXmlParser<T>
where T : BaseItem
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private Dictionary<string, string>? _validProviderIds;
/// <summary>
@@ -144,9 +143,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParse(val, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added))
{
- item.DateCreated = added.ToUniversalTime();
+ item.DateCreated = added;
}
else
{
@@ -179,7 +178,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrEmpty(text))
{
- if (float.TryParse(text, NumberStyles.Any, _usCulture, out var value))
+ if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
item.CriticRating = value;
}
@@ -331,7 +330,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(text))
{
- if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, _usCulture, out var runtime))
+ if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
@@ -534,9 +533,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(firstAired))
{
- if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var airDate) && airDate.Year > 1850)
+ if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
{
- item.PremiereDate = airDate.ToUniversalTime();
+ item.PremiereDate = airDate;
item.ProductionYear = airDate.Year;
}
}
@@ -551,9 +550,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(firstAired))
{
- if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var airDate) && airDate.Year > 1850)
+ if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
{
- item.EndDate = airDate.ToUniversalTime();
+ item.EndDate = airDate;
}
}
@@ -1094,7 +1093,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var intVal))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
sortOrder = intVal;
}
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index 6a3896eb6..5e7af23fc 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -25,8 +25,6 @@ namespace MediaBrowser.LocalMetadata.Savers
/// </summary>
public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
- private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
/// <summary>
/// Initializes a new instance of the <see cref="BaseXmlSaver"/> class.
/// </summary>
@@ -115,11 +113,19 @@ namespace MediaBrowser.LocalMetadata.Savers
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
+
// On Windows, savint the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+ var fileStreamOptions = new FileStreamOptions()
+ {
+ Mode = FileMode.Create,
+ Access = FileAccess.Write,
+ Share = FileShare.None,
+ PreallocationSize = stream.Length
+ };
+
+ using (var filestream = new FileStream(path, fileStreamOptions))
{
stream.CopyTo(filestream);
}
@@ -205,7 +211,7 @@ namespace MediaBrowser.LocalMetadata.Savers
if (item.CriticRating.HasValue)
{
- writer.WriteElementString("CriticRating", item.CriticRating.Value.ToString(_usCulture));
+ writer.WriteElementString("CriticRating", item.CriticRating.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrEmpty(item.Overview))
@@ -289,12 +295,12 @@ namespace MediaBrowser.LocalMetadata.Savers
if (item.CommunityRating.HasValue)
{
- writer.WriteElementString("Rating", item.CommunityRating.Value.ToString(_usCulture));
+ writer.WriteElementString("Rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
}
if (item.ProductionYear.HasValue && item is not Person)
{
- writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(_usCulture));
+ writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
}
if (item is IHasAspectRatio hasAspectRatio)
@@ -322,7 +328,7 @@ namespace MediaBrowser.LocalMetadata.Savers
{
var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
- writer.WriteElementString("RunningTime", Math.Floor(timespan.TotalMinutes).ToString(_usCulture));
+ writer.WriteElementString("RunningTime", Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
}
if (item.ProviderIds != null)
@@ -395,7 +401,7 @@ namespace MediaBrowser.LocalMetadata.Savers
if (person.SortOrder.HasValue)
{
- writer.WriteElementString("SortOrder", person.SortOrder.Value.ToString(_usCulture));
+ writer.WriteElementString("SortOrder", person.SortOrder.Value.ToString(CultureInfo.InvariantCulture));
}
writer.WriteEndElement();
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index e86e518be..409379c35 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -75,7 +75,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
x => new BdInfoFileInfo(x));
}
- public static IDirectoryInfo FromFileSystemPath(Model.IO.IFileSystem fs, string path)
+ public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
{
return new BdInfoDirectoryInfo(fs, path);
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index a7bcaf544..fbc7ba72f 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
@@ -43,11 +44,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
internal const int DefaultHdrImageExtractionTimeout = 20000;
- /// <summary>
- /// The us culture.
- /// </summary>
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly ILogger<MediaEncoder> _logger;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
@@ -151,6 +147,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <param name="pathType">The path type.</param>
public void UpdateEncoderPath(string path, string pathType)
{
+ var config = _configurationManager.GetEncodingOptions();
+
+ // Filesystem may not be case insensitive, but EncoderAppPathDisplay should always point to a valid file?
+ if (string.IsNullOrEmpty(config.EncoderAppPath)
+ && string.Equals(config.EncoderAppPathDisplay, path, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Existing ffmpeg path is empty and the new path is the same as {EncoderAppPathDisplay}. Skipping", nameof(config.EncoderAppPathDisplay));
+ return;
+ }
+
string newPath;
_logger.LogInformation("Attempting to update encoder path to {Path}. pathType: {PathType}", path ?? string.Empty, pathType ?? string.Empty);
@@ -165,19 +171,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
// User had cleared the custom path in UI
newPath = string.Empty;
}
- else if (Directory.Exists(path))
- {
- // Given path is directory, so resolve down to filename
- newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
- }
else
{
- newPath = path;
+ if (Directory.Exists(path))
+ {
+ // Given path is directory, so resolve down to filename
+ newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
+ }
+ else
+ {
+ newPath = path;
+ }
+
+ if (!new EncoderValidator(_logger, newPath).ValidateVersion())
+ {
+ throw new ResourceNotFoundException();
+ }
}
// Write the new ffmpeg path to the xml as <EncoderAppPath>
// This ensures its not lost on next startup
- var config = _configurationManager.GetEncodingOptions();
config.EncoderAppPath = newPath;
_configurationManager.SaveConfiguration("encoding", config);
@@ -465,17 +478,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
Protocol = MediaProtocol.File
};
- return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken);
+ return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ".jpg", cancellationToken);
}
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
{
- return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken);
+ return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ".jpg", cancellationToken);
}
- public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken)
+ public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken)
{
- return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken);
+ return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, outputExtension, cancellationToken);
}
private async Task<string> ExtractImage(
@@ -487,6 +500,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool isAudio,
Video3DFormat? threedFormat,
TimeSpan? offset,
+ string outputExtension,
CancellationToken cancellationToken)
{
var inputArgument = GetInputArgument(inputFile, mediaSource);
@@ -496,7 +510,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter.
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, outputExtension, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -509,7 +523,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, outputExtension, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -522,7 +536,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, outputExtension, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -534,17 +548,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, outputExtension, cancellationToken).ConfigureAwait(false);
}
- private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, CancellationToken cancellationToken)
+ private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, string outputExtension, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException(nameof(inputPath));
}
- var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
+ if (string.IsNullOrEmpty(outputExtension))
+ {
+ outputExtension = ".jpg";
+ }
+ else if (outputExtension[0] != '.')
+ {
+ outputExtension = "." + outputExtension;
+ }
+
+ var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar.
@@ -679,7 +702,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
public string GetTimeParameter(TimeSpan time)
{
- return time.ToString(@"hh\:mm\:ss\.fff", _usCulture);
+ return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
}
public async Task ExtractVideoImagesOnInterval(
@@ -696,11 +719,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
var inputArgument = GetInputArgument(inputFile, mediaSource);
- var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture);
+ var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture);
if (maxWidth.HasValue)
{
- var maxWidthParam = maxWidth.Value.ToString(_usCulture);
+ var maxWidthParam = maxWidth.Value.ToString(CultureInfo.InvariantCulture);
vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam);
}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 6da9886a4..a6caca8db 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -23,9 +23,9 @@
<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="libse" Version="3.6.2" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" />
+ <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0-rc.2*" />
<PackageReference Include="UTF.Unknown" Version="2.4.0" />
</ItemGroup>
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 9196fe139..a9e753726 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -63,10 +63,10 @@ namespace MediaBrowser.MediaEncoding.Probing
public static DateTime? GetDictionaryDateTime(IReadOnlyDictionary<string, string> tags, string key)
{
if (tags.TryGetValue(key, out var val)
- && (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var dateTime)
- || DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out dateTime)))
+ && (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime)
+ || DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out dateTime)))
{
- return dateTime.ToUniversalTime();
+ return dateTime;
}
return null;
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 2516aad1c..9279cb220 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -30,7 +30,6 @@ namespace MediaBrowser.MediaEncoding.Probing
private static readonly Regex _performerPattern = new (@"(?<name>.*) \((?<instrument>.*)\)");
- private readonly CultureInfo _usCulture = new ("en-US");
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
@@ -50,7 +49,8 @@ namespace MediaBrowser.MediaEncoding.Probing
"LOONA 1/3",
"LOONA / yyxy",
"LOONA / ODD EYE CIRCLE",
- "K/DA"
+ "K/DA",
+ "22/7"
};
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
@@ -83,7 +83,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(data.Format.BitRate))
{
- if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
info.Bitrate = value;
}
@@ -191,7 +191,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (data.Format != null && !string.IsNullOrEmpty(data.Format.Duration))
{
- info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, _usCulture)).Ticks;
+ info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, CultureInfo.InvariantCulture)).Ticks;
}
FetchWtvInfo(info, data);
@@ -582,7 +582,8 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <returns>MediaAttachments.</returns>
private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
{
- if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)
+ && streamInfo.Disposition?.GetValueOrDefault("attached_pic") != 1)
{
return null;
}
@@ -673,7 +674,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(streamInfo.SampleRate))
{
- if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
stream.SampleRate = value;
}
@@ -689,6 +690,16 @@ namespace MediaBrowser.MediaEncoding.Probing
{
stream.BitDepth = streamInfo.BitsPerRawSample;
}
+
+ if (string.IsNullOrEmpty(stream.Title))
+ {
+ // mp4 missing track title workaround: fall back to handler_name if populated
+ string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name");
+ if (!string.IsNullOrEmpty(handlerName))
+ {
+ stream.Title = handlerName;
+ }
+ }
}
else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase))
{
@@ -697,6 +708,16 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
stream.LocalizedForced = _localization.GetLocalizedString("Forced");
+
+ if (string.IsNullOrEmpty(stream.Title))
+ {
+ // mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
+ string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name");
+ if (!string.IsNullOrEmpty(handlerName) && !string.Equals(handlerName, "SubtitleHandler", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.Title = handlerName;
+ }
+ }
}
else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
{
@@ -802,7 +823,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(streamInfo.BitRate))
{
- if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
bitrate = value;
}
@@ -815,7 +836,7 @@ namespace MediaBrowser.MediaEncoding.Probing
&& (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio)))
{
// If the stream info doesn't have a bitrate get the value from the media format info
- if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
bitrate = value;
}
@@ -921,8 +942,8 @@ namespace MediaBrowser.MediaEncoding.Probing
var parts = (original ?? string.Empty).Split(':');
if (!(parts.Length == 2 &&
- int.TryParse(parts[0], NumberStyles.Any, _usCulture, out var width) &&
- int.TryParse(parts[1], NumberStyles.Any, _usCulture, out var height) &&
+ int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) &&
+ int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) &&
width > 0 &&
height > 0))
{
@@ -1008,11 +1029,11 @@ namespace MediaBrowser.MediaEncoding.Probing
if (parts.Length == 2)
{
- result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture);
+ result = float.Parse(parts[0], CultureInfo.InvariantCulture) / float.Parse(parts[1], CultureInfo.InvariantCulture);
}
else
{
- result = float.Parse(parts[0], _usCulture);
+ result = float.Parse(parts[0], CultureInfo.InvariantCulture);
}
return float.IsNaN(result) ? null : result;
@@ -1039,7 +1060,7 @@ namespace MediaBrowser.MediaEncoding.Probing
// If we got something, parse it
if (!string.IsNullOrEmpty(duration))
{
- data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
+ data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, CultureInfo.InvariantCulture)).Ticks;
}
}
@@ -1101,7 +1122,7 @@ namespace MediaBrowser.MediaEncoding.Probing
return;
}
- info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, _usCulture);
+ info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, CultureInfo.InvariantCulture);
}
private void SetAudioInfoFromTags(MediaInfo audio, IReadOnlyDictionary<string, string> tags)
@@ -1144,7 +1165,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
Name = match.Groups["name"].Value,
Type = PersonType.Actor,
- Role = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value)
+ Role = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value)
});
}
}
@@ -1378,7 +1399,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
var disc = tags.GetValueOrDefault(tagName);
- if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.Split('/')[0], out var discNum))
+ if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum))
{
return discNum;
}
@@ -1443,16 +1464,16 @@ namespace MediaBrowser.MediaEncoding.Probing
.ToArray();
}
- if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, _usCulture, out var parsedYear))
+ if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
{
video.ProductionYear = parsedYear;
}
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
- if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.None, out var parsedDate))
+ if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.AdjustToUniversal, out var parsedDate))
{
- video.PremiereDate = parsedDate.ToUniversalTime();
+ video.PremiereDate = parsedDate;
}
var description = tags.GetValueOrDefault("WM/SubTitleDescription");
@@ -1468,7 +1489,7 @@ namespace MediaBrowser.MediaEncoding.Probing
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
if (string.IsNullOrWhiteSpace(subTitle)
&& !string.IsNullOrWhiteSpace(description)
- && description.AsSpan()[0..Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)].IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
+ && description.AsSpan()[..Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)].Contains(':')) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string[] descriptionParts = description.Split(':');
if (descriptionParts.Length > 0)
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index 24ceb1b57..52c1b6467 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -5,7 +5,7 @@ using System.Threading;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
-using Nikse.SubtitleEdit.Core;
+using Nikse.SubtitleEdit.Core.Common;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
@@ -38,7 +38,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
subRip.LoadSubtitle(subtitle, lines, "untitled");
if (subRip.ErrorCount > 0)
{
- _logger.LogError("{ErrorCount} errors encountered while parsing subtitle.");
+ _logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount);
}
var trackInfo = new SubtitleTrackInfo();
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 6f6178af2..2b2de2ff6 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -11,6 +11,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -195,7 +196,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return AsyncFile.OpenRead(fileInfo.Path);
}
- private async Task<SubtitleInfo> GetReadableFile(
+ internal async Task<SubtitleInfo> GetReadableFile(
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
CancellationToken cancellationToken)
@@ -205,9 +206,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
string outputFormat;
string outputCodec;
- if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
{
// Extract
outputCodec = "copy";
@@ -238,7 +239,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
.TrimStart('.');
- if (TryGetReader(currentFormat, out _))
+ if (!TryGetReader(currentFormat, out _))
{
// Convert
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
@@ -248,12 +249,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
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);
+ // It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
+ return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
}
private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
@@ -683,8 +680,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!string.Equals(text, newText, StringComparison.Ordinal))
{
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO))
+ using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
using (var writer = new StreamWriter(fileStream, encoding))
{
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
@@ -756,7 +752,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- private struct SubtitleInfo
+ internal readonly struct SubtitleInfo
{
public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
{
@@ -766,13 +762,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
IsExternal = isExternal;
}
- public string Path { get; set; }
+ public string Path { get; }
- public MediaProtocol Protocol { get; set; }
+ public MediaProtocol Protocol { get; }
- public string Format { get; set; }
+ public string Format { get; }
- public bool IsExternal { get; set; }
+ public bool IsExternal { get; }
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index ad32cb794..6d56dda91 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -18,14 +18,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.WriteLine("WEBVTT");
- writer.WriteLine(string.Empty);
+ writer.WriteLine();
writer.WriteLine("REGION");
writer.WriteLine("id:subtitle");
writer.WriteLine("width:80%");
writer.WriteLine("lines:3");
writer.WriteLine("regionanchor:50%,100%");
writer.WriteLine("viewportanchor:50%,90%");
- writer.WriteLine(string.Empty);
+ writer.WriteLine();
foreach (var trackEvent in info.TrackEvents)
{
cancellationToken.ThrowIfCancellationRequested();
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 600a44157..e8fd18ae4 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -208,7 +208,6 @@ namespace MediaBrowser.Model.Dlna
if (string.IsNullOrEmpty(orgPn))
{
contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags);
- continue;
}
else
{
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 635420a76..84d99d550 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1229,7 +1229,7 @@ namespace MediaBrowser.Model.Dlna
bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
- return (result, result ? (TranscodeReason?)null : TranscodeReason.ContainerBitrateExceedsLimit);
+ return (result, result ? null : TranscodeReason.ContainerBitrateExceedsLimit);
}
public static SubtitleProfile GetSubtitleProfile(
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index b213e7aa0..e00157dce 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.Globalization;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Globalization
@@ -56,10 +55,10 @@ namespace MediaBrowser.Model.Globalization
IEnumerable<LocalizationOption> GetLocalizationOptions();
/// <summary>
- /// Returns the correct <see cref="CultureInfo" /> for the given language.
+ /// Returns the correct <see cref="CultureDto" /> for the given language.
/// </summary>
/// <param name="language">The language.</param>
- /// <returns>The correct <see cref="CultureInfo" /> for the given language.</returns>
+ /// <returns>The correct <see cref="CultureDto" /> for the given language.</returns>
CultureDto? FindLanguageInfo(string language);
}
}
diff --git a/MediaBrowser.Model/IO/AsyncFile.cs b/MediaBrowser.Model/IO/AsyncFile.cs
index b888a4163..3c8007d1c 100644
--- a/MediaBrowser.Model/IO/AsyncFile.cs
+++ b/MediaBrowser.Model/IO/AsyncFile.cs
@@ -1,4 +1,3 @@
-using System;
using System.IO;
namespace MediaBrowser.Model.IO
@@ -9,11 +8,23 @@ namespace MediaBrowser.Model.IO
public static class AsyncFile
{
/// <summary>
- /// Gets a value indicating whether we should use async IO on this platform.
- /// <see href="https://github.com/dotnet/runtime/issues/16354" />.
+ /// Gets the default <see cref="FileStreamOptions"/> for reading files async.
/// </summary>
- /// <returns>Returns <c>false</c> on Windows; otherwise <c>true</c>.</returns>
- public static bool UseAsyncIO => !OperatingSystem.IsWindows();
+ public static FileStreamOptions ReadOptions => new FileStreamOptions()
+ {
+ Options = FileOptions.Asynchronous
+ };
+
+ /// <summary>
+ /// Gets the default <see cref="FileStreamOptions"/> for writing files async.
+ /// </summary>
+ public static FileStreamOptions WriteOptions => new FileStreamOptions()
+ {
+ Mode = FileMode.OpenOrCreate,
+ Access = FileAccess.Write,
+ Share = FileShare.None,
+ Options = FileOptions.Asynchronous
+ };
/// <summary>
/// Opens an existing file for reading.
@@ -21,7 +32,7 @@ namespace MediaBrowser.Model.IO
/// <param name="path">The file to be opened for reading.</param>
/// <returns>A read-only <see cref="FileStream" /> on the specified path.</returns>
public static FileStream OpenRead(string path)
- => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, UseAsyncIO);
+ => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
/// <summary>
/// Opens an existing file for writing.
@@ -29,6 +40,6 @@ namespace MediaBrowser.Model.IO
/// <param name="path">The file to be opened for writing.</param>
/// <returns>An unshared <see cref="FileStream" /> object on the specified path with Write access.</returns>
public static FileStream OpenWrite(string path)
- => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, UseAsyncIO);
+ => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index a371afc2c..16bc4adf8 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -14,18 +14,13 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
- <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
- </PropertyGroup>
-
- <PropertyGroup Condition=" '$(Configuration)' == 'Release'">
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@@ -35,9 +30,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0-rc.2*" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
- <PackageReference Include="System.Text.Json" Version="5.0.2" />
+ <PackageReference Include="System.Text.Json" Version="6.0.0-rc.2*" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 96f5ab51a..748170a0e 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -2,8 +2,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using Jellyfin.Extensions;
namespace MediaBrowser.Model.Net
{
@@ -163,15 +165,16 @@ namespace MediaBrowser.Model.Net
return dict;
}
- public static string? GetMimeType(string path) => GetMimeType(path, true);
+ public static string GetMimeType(string path) => GetMimeType(path, "application/octet-stream");
/// <summary>
/// Gets the type of the MIME.
/// </summary>
/// <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)
+ /// <param name="defaultValue">The default value to return if no fitting MIME type is found.</param>
+ /// <returns>The correct MIME type for the given filename, or <paramref name="defaultValue"/> if it wasn't found.</returns>
+ [return: NotNullIfNotNullAttribute("defaultValue")]
+ public static string? GetMimeType(string filename, string? defaultValue = null)
{
if (filename.Length == 0)
{
@@ -210,7 +213,7 @@ namespace MediaBrowser.Model.Net
return "application/octet-stream";
}
- return enableStreamDefault ? "application/octet-stream" : null;
+ return defaultValue;
}
public static string? ToExtension(string mimeType)
@@ -221,7 +224,7 @@ namespace MediaBrowser.Model.Net
}
// handle text/html; charset=UTF-8
- mimeType = mimeType.Split(';')[0];
+ mimeType = mimeType.AsSpan().LeftPart(';').ToString();
if (_extensionLookup.TryGetValue(mimeType, out string? result))
{
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 6c14c8de1..3d8dd1486 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -29,8 +29,6 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
public class ImageSaver
{
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
/// <summary>
/// The _config.
/// </summary>
@@ -263,8 +261,10 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
- // 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, AsyncFile.UseAsyncIO))
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = source.Length;
+ await using (var fs = new FileStream(path, fileStreamOptions))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
@@ -377,7 +377,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-landscape" + extension;
@@ -400,7 +400,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-banner" + extension;
@@ -495,12 +495,12 @@ namespace MediaBrowser.Providers.Manager
var filenames = images.Select(i => Path.GetFileNameWithoutExtension(i.Path)).ToList();
var current = 1;
- while (filenames.Contains(numberedIndexPrefix + current.ToString(UsCulture), StringComparer.OrdinalIgnoreCase))
+ while (filenames.Contains(numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
{
current++;
}
- return numberedIndexPrefix + current.ToString(UsCulture);
+ return numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture);
}
/// <summary>
@@ -539,7 +539,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
@@ -556,7 +556,7 @@ namespace MediaBrowser.Providers.Manager
if (item.IsInMixedFolder)
{
- return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(UsCulture), extension) };
+ return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(CultureInfo.InvariantCulture), extension) };
}
var extraFanartFilename = GetBackdropSaveFilename(item.GetImages(ImageType.Backdrop), "fanart", "fanart", outputIndex);
@@ -568,7 +568,7 @@ namespace MediaBrowser.Providers.Manager
if (EnableExtraThumbsDuplication)
{
- list.Add(Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + outputIndex.ToString(UsCulture) + extension));
+ list.Add(Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + outputIndex.ToString(CultureInfo.InvariantCulture) + extension));
}
return list.ToArray();
@@ -582,7 +582,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-poster" + extension;
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 7fdef6b44..3593697d7 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -163,7 +163,7 @@ namespace MediaBrowser.Providers.Manager
{
var mimeType = MimeTypes.GetMimeType(response.Path);
- var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ var stream = AsyncFile.OpenRead(response.Path);
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
}
@@ -486,7 +486,20 @@ namespace MediaBrowser.Providers.Manager
try
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
+
+ // Sometimes providers send back bad urls. Just move to the next image
+ if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
+ continue;
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
+ break;
+ }
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await _providerManager.SaveImage(
@@ -500,15 +513,8 @@ namespace MediaBrowser.Providers.Manager
result.UpdateType |= ItemUpdateType.ImageUpdate;
return true;
}
- catch (HttpRequestException ex)
+ catch (HttpRequestException)
{
- // Sometimes providers send back bad url's. Just move to the next image
- if (ex.StatusCode.HasValue
- && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
- {
- continue;
- }
-
break;
}
}
@@ -588,6 +594,19 @@ namespace MediaBrowser.Providers.Manager
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
+ // Sometimes providers send back bad urls. Just move to the next image
+ if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
+ continue;
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
+ break;
+ }
+
// If there's already an image of the same size, skip it
if (response.Content.Headers.ContentLength.HasValue)
{
@@ -615,15 +634,8 @@ namespace MediaBrowser.Providers.Manager
cancellationToken).ConfigureAwait(false);
result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
}
- catch (HttpRequestException ex)
+ catch (HttpRequestException)
{
- // Sometimes providers send back bad urls. Just move onto the next image
- if (ex.StatusCode.HasValue
- && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
- {
- continue;
- }
-
break;
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index b51a25417..9be887e9c 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -210,8 +210,7 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentNullException(nameof(source));
}
- var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-
+ var fileStream = AsyncFile.OpenRead(source);
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 3a6e16274..15badfad7 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,9 +16,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" />
@@ -26,7 +26,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
new file mode 100644
index 000000000..1d9d1e02a
--- /dev/null
+++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+ /// <summary>
+ /// Uses <see cref="IMediaEncoder"/> to extract embedded images.
+ /// </summary>
+ public class EmbeddedImageProvider : IDynamicImageProvider, IHasOrder
+ {
+ private static readonly string[] _primaryImageFileNames =
+ {
+ "poster",
+ "folder",
+ "cover",
+ "default"
+ };
+
+ private static readonly string[] _backdropImageFileNames =
+ {
+ "backdrop",
+ "fanart",
+ "background",
+ "art"
+ };
+
+ private static readonly string[] _logoImageFileNames =
+ {
+ "logo",
+ };
+
+ private readonly IMediaEncoder _mediaEncoder;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EmbeddedImageProvider"/> class.
+ /// </summary>
+ /// <param name="mediaEncoder">The media encoder for extracting attached/embedded images.</param>
+ public EmbeddedImageProvider(IMediaEncoder mediaEncoder)
+ {
+ _mediaEncoder = mediaEncoder;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Embedded Image Extractor";
+
+ /// <inheritdoc />
+ // Default to after internet image providers but before Screen Grabber
+ public int Order => 99;
+
+ /// <inheritdoc />
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ if (item is Video)
+ {
+ if (item is Episode)
+ {
+ return new[]
+ {
+ ImageType.Primary,
+ };
+ }
+
+ return new[]
+ {
+ ImageType.Primary,
+ ImageType.Backdrop,
+ ImageType.Logo,
+ };
+ }
+
+ return Array.Empty<ImageType>();
+ }
+
+ /// <inheritdoc />
+ public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
+ {
+ var video = (Video)item;
+
+ // No support for these
+ if (video.IsPlaceHolder || video.VideoType == VideoType.Dvd)
+ {
+ return Task.FromResult(new DynamicImageResponse { HasImage = false });
+ }
+
+ return GetEmbeddedImage(video, type, cancellationToken);
+ }
+
+ private async Task<DynamicImageResponse> GetEmbeddedImage(Video item, ImageType type, CancellationToken cancellationToken)
+ {
+ MediaSourceInfo mediaSource = new MediaSourceInfo
+ {
+ VideoType = item.VideoType,
+ IsoType = item.IsoType,
+ Protocol = item.PathProtocol ?? MediaProtocol.File,
+ };
+
+ string[] imageFileNames = type switch
+ {
+ ImageType.Primary => _primaryImageFileNames,
+ ImageType.Backdrop => _backdropImageFileNames,
+ ImageType.Logo => _logoImageFileNames,
+ _ => _primaryImageFileNames
+ };
+
+ // Try attachments first
+ var attachmentStream = item.GetMediaSources(false)
+ .SelectMany(source => source.MediaAttachments)
+ .FirstOrDefault(attachment => !string.IsNullOrEmpty(attachment.FileName)
+ && imageFileNames.Any(name => attachment.FileName.Contains(name, StringComparison.OrdinalIgnoreCase)));
+
+ if (attachmentStream != null)
+ {
+ return await ExtractAttachment(item, cancellationToken, attachmentStream, mediaSource);
+ }
+
+ // Fall back to EmbeddedImage streams
+ var imageStreams = item.GetMediaStreams().FindAll(i => i.Type == MediaStreamType.EmbeddedImage);
+
+ if (imageStreams.Count == 0)
+ {
+ // Can't extract if we don't have any EmbeddedImage streams
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ // Extract first stream containing an element of imageFileNames
+ var imageStream = imageStreams
+ .FirstOrDefault(stream => !string.IsNullOrEmpty(stream.Comment)
+ && imageFileNames.Any(name => stream.Comment.Contains(name, StringComparison.OrdinalIgnoreCase)));
+
+ // Primary type only: default to first image if none found by label
+ if (imageStream == null)
+ {
+ if (type == ImageType.Primary)
+ {
+ imageStream = imageStreams[0];
+ }
+ else
+ {
+ // No streams matched, abort
+ return new DynamicImageResponse { HasImage = false };
+ }
+ }
+
+ string extractedImagePath =
+ await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, ".jpg", cancellationToken)
+ .ConfigureAwait(false);
+
+ return new DynamicImageResponse
+ {
+ Format = ImageFormat.Jpg,
+ HasImage = true,
+ Path = extractedImagePath,
+ Protocol = MediaProtocol.File
+ };
+ }
+
+ private async Task<DynamicImageResponse> ExtractAttachment(Video item, CancellationToken cancellationToken, MediaAttachment attachmentStream, MediaSourceInfo mediaSource)
+ {
+ var extension = string.IsNullOrEmpty(attachmentStream.MimeType)
+ ? Path.GetExtension(attachmentStream.FileName)
+ : MimeTypes.ToExtension(attachmentStream.MimeType);
+
+ if (string.IsNullOrEmpty(extension))
+ {
+ extension = ".jpg";
+ }
+
+ string extractedAttachmentPath =
+ await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken)
+ .ConfigureAwait(false);
+
+ ImageFormat format = extension switch
+ {
+ ".bmp" => ImageFormat.Bmp,
+ ".gif" => ImageFormat.Gif,
+ ".jpg" => ImageFormat.Jpg,
+ ".png" => ImageFormat.Png,
+ ".webp" => ImageFormat.Webp,
+ _ => ImageFormat.Jpg
+ };
+
+ return new DynamicImageResponse
+ {
+ Format = format,
+ HasImage = true,
+ Path = extractedAttachmentPath,
+ Protocol = MediaProtocol.File
+ };
+ }
+
+ /// <inheritdoc />
+ public bool Supports(BaseItem item)
+ {
+ if (item.IsShortcut)
+ {
+ return false;
+ }
+
+ if (!item.IsFileProtocol)
+ {
+ return false;
+ }
+
+ return item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 1f17d8cd4..261c012a1 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -557,6 +557,7 @@ namespace MediaBrowser.Providers.MediaInfo
subtitleDownloadLanguages,
libraryOptions.DisabledSubtitleFetchers,
libraryOptions.SubtitleFetcherOrder,
+ true,
cancellationToken).ConfigureAwait(false);
// Rescan
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
index 449f0d259..cba2f1c42 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
@@ -36,6 +36,7 @@ namespace MediaBrowser.Providers.MediaInfo
IEnumerable<string> languages,
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
+ bool isAutomated,
CancellationToken cancellationToken)
{
var downloadedLanguages = new List<string>();
@@ -51,6 +52,7 @@ namespace MediaBrowser.Providers.MediaInfo
lang,
disabledSubtitleFetchers,
subtitleFetcherOrder,
+ isAutomated,
cancellationToken).ConfigureAwait(false);
if (downloaded)
@@ -71,6 +73,7 @@ namespace MediaBrowser.Providers.MediaInfo
string lang,
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
+ bool isAutomated,
CancellationToken cancellationToken)
{
if (video.VideoType != VideoType.VideoFile)
@@ -109,6 +112,7 @@ namespace MediaBrowser.Providers.MediaInfo
disabledSubtitleFetchers,
subtitleFetcherOrder,
mediaType,
+ isAutomated,
cancellationToken);
}
@@ -122,6 +126,7 @@ namespace MediaBrowser.Providers.MediaInfo
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
VideoContentType mediaType,
+ bool isAutomated,
CancellationToken cancellationToken)
{
// There's already subtitles for this language
@@ -169,7 +174,8 @@ namespace MediaBrowser.Providers.MediaInfo
IsPerfectMatch = requirePerfectMatch,
DisabledSubtitleFetchers = disabledSubtitleFetchers,
- SubtitleFetcherOrder = subtitleFetcherOrder
+ SubtitleFetcherOrder = subtitleFetcherOrder,
+ IsAutomated = isAutomated
};
if (video is Episode episode)
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 9804ec3bb..4dfe2c59f 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -197,6 +197,7 @@ namespace MediaBrowser.Providers.MediaInfo
subtitleDownloadLanguages,
libraryOptions.DisabledSubtitleFetchers,
libraryOptions.SubtitleFetcherOrder,
+ true,
cancellationToken).ConfigureAwait(false);
// Rescan
diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
index 8b96205c2..d226182c0 100644
--- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
@@ -1,6 +1,3 @@
-#nullable enable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -17,11 +14,19 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo
{
+ /// <summary>
+ /// Uses <see cref="IMediaEncoder"/> to create still images from the main video.
+ /// </summary>
public class VideoImageProvider : IDynamicImageProvider, IHasOrder
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger<VideoImageProvider> _logger;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoImageProvider"/> class.
+ /// </summary>
+ /// <param name="mediaEncoder">The media encoder for capturing images.</param>
+ /// <param name="logger">The logger.</param>
public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger)
{
_mediaEncoder = mediaEncoder;
@@ -71,36 +76,22 @@ namespace MediaBrowser.Providers.MediaInfo
Protocol = item.PathProtocol ?? MediaProtocol.File,
};
- var mediaStreams =
- item.GetMediaStreams();
-
- var imageStreams =
- mediaStreams
- .Where(i => i.Type == MediaStreamType.EmbeddedImage)
- .ToList();
+ // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
+ // Always use 10 seconds for dvd because our duration could be out of whack
+ var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
+ item.RunTimeTicks.Value > 0
+ ? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10)
+ : TimeSpan.FromSeconds(10);
- string extractedImagePath;
+ var videoStream = item.GetDefaultVideoStream() ?? item.GetMediaStreams().FirstOrDefault(i => i.Type == MediaStreamType.Video);
- if (imageStreams.Count == 0)
+ if (videoStream == null)
{
- // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
- // Always use 10 seconds for dvd because our duration could be out of whack
- var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
- item.RunTimeTicks.Value > 0
- ? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10)
- : TimeSpan.FromSeconds(10);
-
- var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
- extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("Skipping image extraction: no video stream found for {Path}.", item.Path ?? string.Empty);
+ return new DynamicImageResponse { HasImage = false };
}
- else
- {
- var imageStream = imageStreams.Find(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase))
- ?? imageStreams.Find(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase))
- ?? imageStreams[0];
- extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, cancellationToken).ConfigureAwait(false);
- }
+ string extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
return new DynamicImageResponse
{
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
index c1226febf..ebdcb94eb 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
@@ -172,8 +172,11 @@ 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);
- // 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, AsyncFile.UseAsyncIO);
+
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = stream.Length;
+ await using var xmlFileStream = new FileStream(path, fileStreamOptions);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index 8572b3413..321013b17 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -154,8 +154,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
- // 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, AsyncFile.UseAsyncIO);
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = stream.Length;
+ await using var xmlFileStream = new FileStream(path, fileStreamOptions);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
index 268538815..19d90b9a1 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
@@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (reader.TokenType == JsonTokenType.String)
{
var str = reader.GetString();
- if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ if (str == null || str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
{
return null;
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 1dea3dece..b2bc58eea 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -26,7 +26,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IApplicationHost _appHost;
private readonly JsonSerializerOptions _jsonOptions;
@@ -79,7 +78,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
+ && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var year)
&& year >= 0)
{
item.ProductionYear = year;
@@ -93,14 +92,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.imdbVotes)
- && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount)
+ && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
&& voteCount >= 0)
{
// item.VoteCount = voteCount;
}
if (!string.IsNullOrEmpty(result.imdbRating)
- && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating)
+ && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
&& imdbRating >= 0)
{
item.CommunityRating = imdbRating;
@@ -191,7 +190,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
+ && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var year)
&& year >= 0)
{
item.ProductionYear = year;
@@ -205,14 +204,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.imdbVotes)
- && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount)
+ && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
&& voteCount >= 0)
{
// item.VoteCount = voteCount;
}
if (!string.IsNullOrEmpty(result.imdbRating)
- && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating)
+ && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
&& imdbRating >= 0)
{
item.CommunityRating = imdbRating;
@@ -295,7 +294,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam));
var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
@@ -335,7 +334,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
seasonId));
var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index a5287e749..fc6af0b34 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index 1bda1a09b..ca44c9bbc 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
index 7a057c065..a1873eaae 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
@@ -146,7 +146,7 @@ namespace MediaBrowser.Providers.Studios
Directory.CreateDirectory(Path.GetDirectoryName(file));
await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
@@ -172,19 +172,16 @@ namespace MediaBrowser.Providers.Studios
public IEnumerable<string> GetAvailableImages(string file)
{
- using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using var fileStream = File.OpenRead(file);
using var reader = new StreamReader(fileStream);
- var lines = new List<string>();
foreach (var line in reader.ReadAllLines())
{
if (!string.IsNullOrWhiteSpace(line))
{
- lines.Add(line);
+ yield return line;
}
}
-
- return lines;
}
}
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index d6c346ba1..73f9d7b62 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -187,8 +187,8 @@ namespace MediaBrowser.Providers.Subtitles
{
var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
- using var stream = response.Stream;
- using var memoryStream = new MemoryStream();
+ await using var stream = response.Stream;
+ await using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0;
@@ -236,7 +236,7 @@ namespace MediaBrowser.Providers.Subtitles
foreach (var savePath in savePaths)
{
- _logger.LogInformation("Saving subtitles to {0}", savePath);
+ _logger.LogInformation("Saving subtitles to {SavePath}", savePath);
_monitor.ReportFileSystemChangeBeginning(savePath);
@@ -244,8 +244,10 @@ namespace MediaBrowser.Providers.Subtitles
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ var fileOptions = AsyncFile.WriteOptions;
+ fileOptions.Mode = FileMode.CreateNew;
+ fileOptions.PreallocationSize = stream.Length;
+ using var fs = new FileStream(savePath, fileOptions);
await stream.CopyToAsync(fs).ConfigureAwait(false);
return;
@@ -254,13 +256,9 @@ namespace MediaBrowser.Providers.Subtitles
{
// Bug in analyzer -- https://github.com/dotnet/roslyn-analyzers/issues/5160
#pragma warning disable CA1508
- exs ??= new List<Exception>()
- {
- ex
- };
+ (exs ??= new List<Exception>()).Add(ex);
#pragma warning restore CA1508
-
- }
+ }
finally
{
_monitor.ReportFileSystemChangeComplete(savePath, false);
@@ -276,7 +274,7 @@ namespace MediaBrowser.Providers.Subtitles
}
/// <inheritdoc />
- public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, CancellationToken cancellationToken)
+ public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, bool isAutomated, CancellationToken cancellationToken)
{
if (video.VideoType != VideoType.VideoFile)
{
@@ -310,7 +308,8 @@ namespace MediaBrowser.Providers.Subtitles
ProductionYear = video.ProductionYear,
ProviderIds = video.ProviderIds,
RuntimeTicks = video.RunTimeTicks,
- IsPerfectMatch = isPerfectMatch ?? false
+ IsPerfectMatch = isPerfectMatch ?? false,
+ IsAutomated = isAutomated
};
if (video is Episode episode)
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 3e2a9bacf..926be5a92 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -15,7 +15,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index f975278fb..9d558b6ce 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Providers;
using MediaBrowser.Controller.Entities;
@@ -57,8 +58,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
_directoryService = directoryService;
}
- protected CultureInfo UsCulture { get; } = new CultureInfo("en-US");
-
/// <summary>
/// Gets the logger.
/// </summary>
@@ -267,9 +266,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- item.DateCreated = added.ToUniversalTime();
+ item.DateCreated = added;
}
else
{
@@ -308,7 +307,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrEmpty(text))
{
- if (float.TryParse(text, NumberStyles.Any, UsCulture, out var value))
+ if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
item.CriticRating = value;
}
@@ -369,7 +368,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val) && userData != null)
{
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var count))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count))
{
userData.PlayCount = count;
}
@@ -383,9 +382,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val) && userData != null)
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- userData.LastPlayedDate = added.ToUniversalTime();
+ userData.LastPlayedDate = added;
}
else
{
@@ -474,7 +473,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(text))
{
- if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, UsCulture, out var runtime))
+ if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
@@ -684,9 +683,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- item.PremiereDate = date.ToUniversalTime();
+ item.PremiereDate = date;
item.ProductionYear = date.Year;
}
}
@@ -702,9 +701,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- item.EndDate = date.ToUniversalTime();
+ item.EndDate = date;
}
}
@@ -1264,7 +1263,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var intVal))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
sortOrder = intVal;
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index 3a305024e..d2f349ad7 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -163,7 +163,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeEpisodeNumber = rval;
}
@@ -179,7 +179,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsAfterSeasonNumber = rval;
}
@@ -195,7 +195,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeSeasonNumber = rval;
}
@@ -211,7 +211,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeSeasonNumber = rval;
}
@@ -227,7 +227,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeEpisodeNumber = rval;
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 38726a6f0..594402258 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -203,8 +203,15 @@ namespace MediaBrowser.XbmcMetadata.Savers
// On Windows, saving the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+ var fileStreamOptions = new FileStreamOptions()
+ {
+ Mode = FileMode.Create,
+ Access = FileAccess.Write,
+ Share = FileShare.None,
+ PreallocationSize = stream.Length
+ };
+
+ using (var filestream = new FileStream(path, fileStreamOptions))
{
stream.CopyTo(filestream);
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
index 62f80e81b..2cd3fdf02 100644
--- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
@@ -17,8 +17,6 @@ namespace MediaBrowser.XbmcMetadata.Savers
/// </summary>
public class EpisodeNfoSaver : BaseNfoSaver
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeNfoSaver"/> class.
/// </summary>
@@ -60,17 +58,17 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (episode.IndexNumber.HasValue)
{
- writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.IndexNumberEnd.HasValue)
{
- writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(_usCulture));
+ writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.ParentIndexNumber.HasValue)
{
- writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.PremiereDate.HasValue)
@@ -84,28 +82,28 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
if (episode.AirsAfterSeasonNumber.HasValue && episode.AirsAfterSeasonNumber.Value != -1)
{
- writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1)
{
- writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.AirsBeforeSeasonNumber.HasValue && episode.AirsBeforeSeasonNumber.Value != -1)
{
- writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1)
{
- writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(CultureInfo.InvariantCulture));
}
var specialSeason = episode.AiredSeasonNumber;
if (specialSeason.HasValue && specialSeason.Value != -1)
{
- writer.WriteElementString("displayseason", specialSeason.Value.ToString(_usCulture));
+ writer.WriteElementString("displayseason", specialSeason.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
diff --git a/README.md b/README.md
index 3aef84b99..6653bff11 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,7 @@ These instructions will help you get set up with a local development environment
### Prerequisites
-Before the project can be built, you must first install the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) on your system.
+Before the project can be built, you must first install the [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system.
Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 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).
diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs
index 7d6a471f9..5d7da4124 100644
--- a/RSSDP/DisposableManagedObjectBase.cs
+++ b/RSSDP/DisposableManagedObjectBase.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Text;
namespace Rssdp.Infrastructure
@@ -45,11 +46,11 @@ namespace Rssdp.Infrastructure
const string ArgFormat = "{0}: {1}\r\n";
- builder.AppendFormat("{0}\r\n", header);
+ builder.AppendFormat(CultureInfo.InvariantCulture, "{0}\r\n", header);
foreach (var pair in values)
{
- builder.AppendFormat(ArgFormat, pair.Key, pair.Value);
+ builder.AppendFormat(CultureInfo.InvariantCulture, ArgFormat, pair.Key, pair.Value);
}
builder.Append("\r\n");
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
index 54113d464..77130983b 100644
--- a/RSSDP/RSSDP.csproj
+++ b/RSSDP/RSSDP.csproj
@@ -11,7 +11,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
<Nullable>disable</Nullable>
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index e0116c068..58dabc628 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -395,7 +395,7 @@ namespace Rssdp.Infrastructure
// Strange cannot convert compiler error here if I don't explicitly
// assign or cast to Action first. Assignment is easier to read,
// so went with that.
- ProcessMessage(System.Text.UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint, result.LocalIPAddress);
+ ProcessMessage(UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint, result.LocalIPAddress);
}
}
catch (ObjectDisposedException)
diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs
index 4005d836d..c826830f1 100644
--- a/RSSDP/SsdpDevice.cs
+++ b/RSSDP/SsdpDevice.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Globalization;
using Rssdp.Infrastructure;
namespace Rssdp
@@ -134,11 +135,13 @@ namespace Rssdp
{
get
{
- return String.Format("urn:{0}:{3}:{1}:{2}",
- this.DeviceTypeNamespace ?? String.Empty,
- this.DeviceType ?? String.Empty,
- this.DeviceVersion,
- this.DeviceClass ?? "device");
+ return String.Format(
+ CultureInfo.InvariantCulture,
+ "urn:{0}:{3}:{1}:{2}",
+ this.DeviceTypeNamespace ?? String.Empty,
+ this.DeviceType ?? String.Empty,
+ this.DeviceVersion,
+ this.DeviceClass ?? "device");
}
}
diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs
index 188e298e2..3a52b2a3e 100644
--- a/RSSDP/SsdpDeviceLocator.cs
+++ b/RSSDP/SsdpDeviceLocator.cs
@@ -513,7 +513,7 @@ namespace Rssdp.Infrastructure
return TimeSpan.Zero;
}
- return (TimeSpan)(headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero);
+ return headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero;
}
private void RemoveExpiredDevicesFromCache()
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index c9e795d56..64d19803d 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
@@ -233,7 +234,7 @@ namespace Rssdp.Infrastructure
{
if (String.IsNullOrEmpty(searchTarget))
{
- WriteTrace(String.Format("Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
+ WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
return;
}
@@ -340,7 +341,7 @@ namespace Rssdp.Infrastructure
private string GetUsn(string udn, string fullDeviceType)
{
- return String.Format("{0}::{1}", udn, fullDeviceType);
+ return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
}
private async void SendSearchResponse(
@@ -363,7 +364,7 @@ namespace Rssdp.Infrastructure
values["DATE"] = DateTime.UtcNow.ToString("r");
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
values["ST"] = searchTarget;
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["USN"] = uniqueServiceName;
values["LOCATION"] = rootDevice.Location.ToString();
@@ -497,7 +498,7 @@ namespace Rssdp.Infrastructure
values["DATE"] = DateTime.UtcNow.ToString("r");
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
values["LOCATION"] = rootDevice.Location.ToString();
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["NTS"] = "ssdp:alive";
values["NT"] = notificationType;
values["USN"] = uniqueServiceName;
@@ -522,7 +523,7 @@ namespace Rssdp.Infrastructure
}
tasks.Add(SendByeByeNotification(device, device.Udn, device.Udn, cancellationToken));
- tasks.Add(SendByeByeNotification(device, String.Format("urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken));
+ tasks.Add(SendByeByeNotification(device, String.Format(CultureInfo.InvariantCulture, "urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken));
foreach (var childDevice in device.Devices)
{
@@ -542,7 +543,7 @@ namespace Rssdp.Infrastructure
// If needed later for non-server devices, these headers will need to be dynamic
values["HOST"] = "239.255.255.250:1900";
values["DATE"] = DateTime.UtcNow.ToString("r");
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["NTS"] = "ssdp:byebye";
values["NT"] = notificationType;
values["USN"] = uniqueServiceName;
@@ -550,7 +551,7 @@ namespace Rssdp.Infrastructure
var message = BuildMessage(header, values);
var sendCount = IsDisposed ? 1 : 3;
- WriteTrace(String.Format("Sent byebye notification"), device);
+ WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
}
diff --git a/bump_version b/bump_version
index 8012ec07f..f615606e2 100755
--- a/bump_version
+++ b/bump_version
@@ -21,7 +21,14 @@ fi
shared_version_file="./SharedVersion.cs"
build_file="./build.yaml"
# csproj files for nuget packages
-jellyfin_subprojects=( MediaBrowser.Common/MediaBrowser.Common.csproj Jellyfin.Data/Jellyfin.Data.csproj MediaBrowser.Controller/MediaBrowser.Controller.csproj MediaBrowser.Model/MediaBrowser.Model.csproj Emby.Naming/Emby.Naming.csproj )
+jellyfin_subprojects=(
+ MediaBrowser.Common/MediaBrowser.Common.csproj
+ Jellyfin.Data/Jellyfin.Data.csproj
+ MediaBrowser.Controller/MediaBrowser.Controller.csproj
+ MediaBrowser.Model/MediaBrowser.Model.csproj
+ Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+)
new_version="$1"
diff --git a/debian/control b/debian/control
index 51b20c670..da9aa94d4 100644
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Section: misc
Priority: optional
Maintainer: Jellyfin Team <team@jellyfin.org>
Build-Depends: debhelper (>= 9),
- dotnet-sdk-5.0,
+ dotnet-sdk-6.0,
libc6-dev,
libcurl4-openssl-dev,
libfontconfig1-dev,
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 326e995be..78f051e4f 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -2,7 +2,6 @@ FROM centos:7
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,12 +10,13 @@ ENV IS_DOCKER=YES
# Prepare CentOS environment
RUN yum update -yq \
&& yum install -yq epel-release \
- && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git
+ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \
- && rpmdev-setuptree \
- && yum install -yq dotnet-sdk-${SDK_VERSION}
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Create symlinks and directories
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.amd64 /build.sh \
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index 23b662526..daba0eb7d 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index a33099031..db4e7f817 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
@@ -18,16 +18,16 @@ RUN apt-get update -yqq \
RUN dpkg --add-architecture arm64 \
&& apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends cross-gcc-dev \
- && TARGET_LIST="arm64" cross-gcc-gensource 8 \
- && cd cross-gcc-packages-amd64/cross-gcc-8-arm64 \
+ && TARGET_LIST="arm64" cross-gcc-gensource 9 \
+ && cd cross-gcc-packages-amd64/cross-gcc-9-arm64 \
&& apt-get install -yqq --no-install-recommends \
- gcc-8-source libstdc++-8-dev-arm64-cross \
+ gcc-9-source libstdc++-9-dev-arm64-cross \
binutils-aarch64-linux-gnu bison flex libtool \
gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \
systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip \
libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 \
libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 \
- libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-8-dev:arm64
+ libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-9-dev:arm64
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.arm64 /build.sh
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index bc5e3543f..9b008e7fb 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
@@ -18,17 +18,17 @@ RUN apt-get update -yqq \
RUN dpkg --add-architecture armhf \
&& apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends cross-gcc-dev \
- && TARGET_LIST="armhf" cross-gcc-gensource 8 \
- && cd cross-gcc-packages-amd64/cross-gcc-8-armhf \
+ && TARGET_LIST="armhf" cross-gcc-gensource 9 \
+ && cd cross-gcc-packages-amd64/cross-gcc-9-armhf \
&& apt-get install -yqq --no-install-recommends\
- gcc-8-source libstdc++-8-dev-armhf-cross \
+ gcc-9-source libstdc++-9-dev-armhf-cross \
binutils-aarch64-linux-gnu bison flex libtool gdb \
sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \
systemtap-sdt-dev autogen expect chrpath zlib1g-dev \
zip binutils-arm-linux-gnueabihf libc6-dev:armhf \
linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf \
libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf \
- liblttng-ust0:armhf libstdc++-8-dev:armhf
+ liblttng-ust0:armhf libstdc++-9-dev:armhf
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh
diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64
index 0b1a57014..b2bd40713 100644
--- a/deployment/Dockerfile.docker.amd64
+++ b/deployment/Dockerfile.docker.amd64
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64
index 583f53ca0..fc60f1624 100644
--- a/deployment/Dockerfile.docker.arm64
+++ b/deployment/Dockerfile.docker.arm64
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf
index 177c11713..f5cc47d83 100644
--- a/deployment/Dockerfile.docker.armhf
+++ b/deployment/Dockerfile.docker.armhf
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 590cde167..14eeb6eed 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -2,7 +2,6 @@ FROM fedora:33
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -10,10 +9,14 @@ ENV IS_DOCKER=YES
# Prepare Fedora environment
RUN dnf update -yq \
- && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd
+ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget
# Install DotNET SDK
-RUN dnf install -yq dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
# Create symlinks and directories
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 3c7e2b87f..2c7e41cac 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
index 3cda9ad23..e903cf1d3 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
index ddf97cbd1..0dd3c5e4e 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
index 49e1c7bbf..16a8218e1 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index fad44ef83..699ab2d40 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 90cc0717b..b567d7bce 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index d88efcdc9..8733be89c 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -18,8 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/13b9d84c-a35b-4ffe-8f62-447a01403d64/1f9ae31daa0f7d98513e7551246899f2/dotnet-sdk-5.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 4f41bba2d..6ae0d53cc 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -17,8 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/13b9d84c-a35b-4ffe-8f62-447a01403d64/1f9ae31daa0f7d98513e7551246899f2/dotnet-sdk-5.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 01752d536..154388148 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -17,8 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/13b9d84c-a35b-4ffe-8f62-447a01403d64/1f9ae31daa0f7d98513e7551246899f2/dotnet-sdk-5.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index acd0e1854..b9543a7c9 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
diff --git a/deployment/build.centos.amd64 b/deployment/build.centos.amd64
index 69f0cadcf..bfdc6e591 100755
--- a/deployment/build.centos.amd64
+++ b/deployment/build.centos.amd64
@@ -8,6 +8,16 @@ set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove BuildRequires for dotnet-sdk-6.0, since it's installed manually
+ pushd fedora
+
+ cp -a jellyfin.spec /tmp/spec.orig
+ sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
+
+ popd
+fi
+
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
@@ -37,4 +47,13 @@ fi
rm -f fedora/jellyfin*.tar.gz
+if [[ ${IS_DOCKER} == YES ]]; then
+ pushd fedora
+
+ cp -a /tmp/spec.orig jellyfin.spec
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+
+ popd
+fi
+
popd
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
index 145e28d87..b2bbf9c29 100755
--- a/deployment/build.debian.amd64
+++ b/deployment/build.debian.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64
index 5699133a0..02f84471e 100755
--- a/deployment/build.debian.arm64
+++ b/deployment/build.debian.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf
index 20af2ddfb..92779cb59 100755
--- a/deployment/build.debian.armhf
+++ b/deployment/build.debian.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64
index 2c7bff506..23c5ed86a 100755
--- a/deployment/build.fedora.amd64
+++ b/deployment/build.fedora.amd64
@@ -8,6 +8,16 @@ set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove BuildRequires for dotnet-sdk-6.0, since it's installed manually
+ pushd fedora
+
+ cp -a jellyfin.spec /tmp/spec.orig
+ sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
+
+ popd
+fi
+
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
@@ -37,4 +47,13 @@ fi
rm -f fedora/jellyfin*.tar.gz
+if [[ ${IS_DOCKER} == YES ]]; then
+ pushd fedora
+
+ cp -a /tmp/spec.orig jellyfin.spec
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+
+ popd
+fi
+
popd
diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64
index 0c29286c0..c36978c9e 100755
--- a/deployment/build.ubuntu.amd64
+++ b/deployment/build.ubuntu.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64
index 65d67f80f..76d51e321 100755
--- a/deployment/build.ubuntu.arm64
+++ b/deployment/build.ubuntu.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf
index 370370abc..0ff5ab066 100755
--- a/deployment/build.ubuntu.armhf
+++ b/deployment/build.ubuntu.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 0d606f9f7..47dee7c13 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
# Requirements not packaged in main repos
# COPR @dotnet-sig/dotnet or
# https://packages.microsoft.com/rhel/7/prod/
-BuildRequires: dotnet-runtime-5.0, dotnet-sdk-5.0
+BuildRequires: dotnet-runtime-6.0, dotnet-sdk-6.0
Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
# Disable Automatic Dependency Processing
AutoReqProv: no
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 981b796e0..3d9538d1b 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -1,17 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PublishRepositoryUrl>true</PublishRepositoryUrl>
+ <EmbedUntrackedSources>true</EmbedUntrackedSources>
+ <IncludeSymbols>true</IncludeSymbols>
+ <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
+ <PackageId>Jellyfin.Extensions</PackageId>
+ <VersionPrefix>10.8.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
+ <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
+ <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
+ <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
+ </PropertyGroup>
+
<ItemGroup>
<Compile Include="../../SharedVersion.cs" />
</ItemGroup>
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index c39805aa3..51b955145 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -44,7 +44,7 @@ namespace Jellyfin.Extensions.Json.Converters
{
try
{
- parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
+ parsedValues[i] = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()) ?? throw new FormatException();
convertedCount++;
}
catch (FormatException)
diff --git a/src/Jellyfin.Extensions/ReadOnlyListExtension.cs b/src/Jellyfin.Extensions/ReadOnlyListExtension.cs
new file mode 100644
index 000000000..7785cfb49
--- /dev/null
+++ b/src/Jellyfin.Extensions/ReadOnlyListExtension.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Extensions
+{
+ /// <summary>
+ /// Static extensions for the <see cref="IReadOnlyList{T}"/> interface.
+ /// </summary>
+ public static class ReadOnlyListExtension
+ {
+ /// <summary>
+ /// Finds the index of the desired item.
+ /// </summary>
+ /// <param name="source">The source list.</param>
+ /// <param name="value">The value to fine.</param>
+ /// <typeparam name="T">The type of item to find.</typeparam>
+ /// <returns>Index if found, else -1.</returns>
+ public static int IndexOf<T>(this IReadOnlyList<T> source, T value)
+ {
+ if (source is IList<T> list)
+ {
+ return list.IndexOf(value);
+ }
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ if (Equals(value, source[i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /// <summary>
+ /// Finds the index of the predicate.
+ /// </summary>
+ /// <param name="source">The source list.</param>
+ /// <param name="match">The value to find.</param>
+ /// <typeparam name="T">The type of item to find.</typeparam>
+ /// <returns>Index if found, else -1.</returns>
+ public static int FindIndex<T>(this IReadOnlyList<T> source, Predicate<T> match)
+ {
+ if (source is List<T> list)
+ {
+ return list.FindIndex(match);
+ }
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ if (match(source[i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+ }
+}
diff --git a/src/Jellyfin.Extensions/ShuffleExtensions.cs b/src/Jellyfin.Extensions/ShuffleExtensions.cs
index 4e481983f..33c492053 100644
--- a/src/Jellyfin.Extensions/ShuffleExtensions.cs
+++ b/src/Jellyfin.Extensions/ShuffleExtensions.cs
@@ -8,8 +8,6 @@ namespace Jellyfin.Extensions
/// </summary>
public static class ShuffleExtensions
{
- private static readonly Random _rng = new Random();
-
/// <summary>
/// Shuffles the items in a list.
/// </summary>
@@ -17,7 +15,7 @@ namespace Jellyfin.Extensions
/// <typeparam name="T">The type.</typeparam>
public static void Shuffle<T>(this IList<T> list)
{
- list.Shuffle(_rng);
+ list.Shuffle(Random.Shared);
}
/// <summary>
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index acc695ed2..3a7707253 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -27,5 +27,39 @@ namespace Jellyfin.Extensions
return count;
}
+
+ /// <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 right 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 right of the <paramref name="needle" />.</returns>
+ public static ReadOnlySpan<char> RightPart(this ReadOnlySpan<char> haystack, char needle)
+ {
+ var pos = haystack.LastIndexOf(needle);
+ if (pos == -1)
+ {
+ return haystack;
+ }
+
+ if (pos == haystack.Length - 1)
+ {
+ return ReadOnlySpan<char>.Empty;
+ }
+
+ return haystack[(pos + 1)..];
+ }
}
}
diff --git a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
index 59a6b52d1..1f06e8fde 100644
--- a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using Jellyfin.Api.Controllers;
using Xunit;
@@ -19,33 +18,28 @@ namespace Jellyfin.Api.Tests.Controllers
}
}
- public static IEnumerable<object[]> GetSegmentLengths_Success_TestData()
+ public static TheoryData<long, int, double[]> GetSegmentLengths_Success_TestData()
{
- yield return new object[] { 0, 6, Array.Empty<double>() };
- yield return new object[]
- {
+ var data = new TheoryData<long, int, double[]>();
+ data.Add(0, 6, Array.Empty<double>());
+ data.Add(
TimeSpan.FromSeconds(3).Ticks,
6,
- new double[] { 3 }
- };
- yield return new object[]
- {
+ new double[] { 3 });
+ data.Add(
TimeSpan.FromSeconds(6).Ticks,
6,
- new double[] { 6 }
- };
- yield return new object[]
- {
+ new double[] { 6 });
+ data.Add(
TimeSpan.FromSeconds(3.3333333).Ticks,
6,
- new double[] { 3.3333333 }
- };
- yield return new object[]
- {
+ new double[] { 3.3333333 });
+ data.Add(
TimeSpan.FromSeconds(9.3333333).Ticks,
6,
- new double[] { 6, 3.3333333 }
- };
+ new double[] { 6, 3.3333333 });
+
+ return data;
}
}
}
diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
index 97e441b1d..4ba7e1d2f 100644
--- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
+++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
@@ -15,16 +15,16 @@ namespace Jellyfin.Api.Tests.Helpers
Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
}
- public static IEnumerable<object[]> GetOrderBy_Success_TestData()
+ public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>();
+
+ data.Add(
Array.Empty<string>(),
Array.Empty<SortOrder>(),
- Array.Empty<(string, SortOrder)>()
- };
- yield return new object[]
- {
+ Array.Empty<(string, SortOrder)>());
+
+ data.Add(
new string[]
{
"IsFavoriteOrLiked",
@@ -35,10 +35,9 @@ namespace Jellyfin.Api.Tests.Helpers
{
("IsFavoriteOrLiked", SortOrder.Ascending),
("Random", SortOrder.Ascending),
- }
- };
- yield return new object[]
- {
+ });
+
+ data.Add(
new string[]
{
"SortName",
@@ -52,8 +51,9 @@ namespace Jellyfin.Api.Tests.Helpers
{
("SortName", SortOrder.Descending),
("ProductionYear", SortOrder.Descending),
- }
- };
+ });
+
+ return data;
}
[Fact]
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 0c36e81cc..922b3d94f 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -15,8 +15,8 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.9" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs
index 18d3f9763..bfece97b6 100644
--- a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs
+++ b/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs
@@ -19,18 +19,16 @@ namespace Jellyfin.Common.Tests.Cryptography
Assert.Throws<ArgumentException>(() => new PasswordHash(string.Empty, Array.Empty<byte>()));
}
- public static IEnumerable<object[]> Parse_Valid_TestData()
+ public static TheoryData<string, PasswordHash> Parse_Valid_TestData()
{
+ var data = new TheoryData<string, PasswordHash>();
// Id
- yield return new object[]
- {
+ data.Add(
"$PBKDF2",
- new PasswordHash("PBKDF2", Array.Empty<byte>())
- };
+ new PasswordHash("PBKDF2", Array.Empty<byte>()));
// Id + parameter
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000",
new PasswordHash(
"PBKDF2",
@@ -39,12 +37,10 @@ namespace Jellyfin.Common.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" },
- })
- };
+ }));
// Id + parameters
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000,m=120",
new PasswordHash(
"PBKDF2",
@@ -54,34 +50,28 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
- })
- };
+ }));
// Id + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Array.Empty<byte>(),
- new Dictionary<string, string>())
- };
+ new Dictionary<string, string>()));
// Id + salt + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Convert.FromHexString("69F420"),
- new Dictionary<string, string>())
- };
+ new Dictionary<string, string>()));
// Id + parameter + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -90,12 +80,9 @@ namespace Jellyfin.Common.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" }
- })
- };
-
+ }));
// Id + parameters + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -105,12 +92,9 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
- })
- };
-
+ }));
// Id + parameters + salt + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -120,8 +104,8 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
- })
- };
+ }));
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 1619fa89c..1fe4e2565 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index a5778b59c..e9a951571 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 5a48631c2..1fb95aab4 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs
index 6fdca4694..d46beedd9 100644
--- a/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs
@@ -6,10 +6,17 @@ namespace Jellyfin.Extensions.Tests
{
public static class CopyToExtensionsTests
{
- public static IEnumerable<object[]> CopyTo_Valid_Correct_TestData()
+ public static TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>> CopyTo_Valid_Correct_TestData()
{
- yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } };
- yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } };
+ var data = new TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>>();
+
+ data.Add(
+ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 });
+
+ data.Add(
+ new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } );
+
+ return data;
}
[Theory]
@@ -20,13 +27,26 @@ namespace Jellyfin.Extensions.Tests
Assert.Equal(expected, destination);
}
- public static IEnumerable<object[]> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
+ public static TheoryData<IReadOnlyList<int>, IList<int>, int> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
{
- yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 };
- yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 };
- yield return new object[] { new[] { 0, 1, 2 }, Array.Empty<int>(), 0 };
- yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 };
- yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 };
+ var data = new TheoryData<IReadOnlyList<int>, IList<int>, int>();
+
+ data.Add(
+ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 );
+
+ data.Add(
+ new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 );
+
+ data.Add(
+ new[] { 0, 1, 2 }, Array.Empty<int>(), 0 );
+
+ data.Add(
+ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 );
+
+ data.Add(
+ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 );
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 20680157f..2dc4ac19a 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs
index c72216d94..a73cfb078 100644
--- a/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs
@@ -5,13 +5,11 @@ namespace Jellyfin.Extensions.Tests
{
public static class ShuffleExtensionsTests
{
- private static readonly Random _rng = new Random();
-
[Fact]
public static void Shuffle_Valid_Correct()
{
byte[] original = new byte[1 << 6];
- _rng.NextBytes(original);
+ Random.Shared.NextBytes(original);
byte[] shuffled = (byte[])original.Clone();
shuffled.Shuffle();
diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
index d1aa2e476..7186cc023 100644
--- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
@@ -14,5 +14,28 @@ namespace Jellyfin.Extensions.Tests
{
Assert.Equal(count, str.AsSpan().Count(needle));
}
+
+ [Theory]
+ [InlineData("", 'q', "")]
+ [InlineData("Banana split", ' ', "Banana")]
+ [InlineData("Banana split", 'q', "Banana split")]
+ [InlineData("Banana split 2", ' ', "Banana")]
+ public void LeftPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
+ {
+ var result = str.AsSpan().LeftPart(needle).ToString();
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData("", 'q', "")]
+ [InlineData("Banana split", ' ', "split")]
+ [InlineData("Banana split", 'q', "Banana split")]
+ [InlineData("Banana split.", '.', "")]
+ [InlineData("Banana split 2", ' ', "2")]
+ public void RightPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
+ {
+ var result = str.AsSpan().RightPart(needle).ToString();
+ Assert.Equal(expectedResult, result);
+ }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index d1854a3c8..c0c363d3d 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections;
-using System.Collections.Generic;
using MediaBrowser.MediaEncoding.Encoder;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
@@ -34,23 +32,21 @@ namespace Jellyfin.MediaEncoding.Tests
Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput));
}
- private class GetFFmpegVersionTestData : IEnumerable<object?[]>
+ private class GetFFmpegVersionTestData : TheoryData<string, Version?>
{
- public IEnumerator<object?[]> GetEnumerator()
+ public GetFFmpegVersionTestData()
{
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 0) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput, null };
+ Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4));
+ Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2));
+ Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1));
+ Add(EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3));
+ Add(EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1));
+ Add(EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2));
+ Add(EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4));
+ Add(EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4));
+ Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 0));
+ Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput, null);
}
-
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 7ea503913..201f63a2d 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -18,10 +18,14 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.1.0" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index d002d5a34..4504924cb 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -5,8 +5,10 @@ using System.Text.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests.Probing
@@ -56,6 +58,72 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
}
[Fact]
+ public void GetMediaInfo_Mp4MetaData_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_mp4_metadata.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+
+ // subtitle handling requires a localization object, set a mock to return the input string
+ var mockLocalization = new Mock<ILocalizationManager>();
+ mockLocalization.Setup(x => x.GetLocalizedString(It.IsAny<string>())).Returns<string>(x => x);
+ ProbeResultNormalizer localizedProbeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), mockLocalization.Object);
+
+ MediaInfo res = localizedProbeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_mp4_metadata.mkv", MediaProtocol.File);
+
+ // [Video, Audio (Main), Audio (Commentary), Subtitle (Main, Spanish), Subtitle (Main, English), Subtitle (Commentary)
+ Assert.Equal(6, res.MediaStreams.Count);
+
+ Assert.NotNull(res.VideoStream);
+ Assert.Equal(res.MediaStreams[0], res.VideoStream);
+ Assert.Equal(0, res.VideoStream.Index);
+ Assert.Equal("h264", res.VideoStream.Codec);
+ Assert.Equal("High", res.VideoStream.Profile);
+ Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
+ Assert.Equal(358, res.VideoStream.Height);
+ Assert.Equal(720, res.VideoStream.Width);
+ Assert.Equal("2.40:1", res.VideoStream.AspectRatio);
+ Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
+ Assert.Equal(31d, res.VideoStream.Level);
+ Assert.Equal(1, res.VideoStream.RefFrames);
+ Assert.True(res.VideoStream.IsAVC);
+ Assert.Equal(120f, res.VideoStream.RealFrameRate);
+ Assert.Equal("1/90000", res.VideoStream.TimeBase);
+ Assert.Equal(1147365, res.VideoStream.BitRate);
+ Assert.Equal(8, res.VideoStream.BitDepth);
+ Assert.True(res.VideoStream.IsDefault);
+ Assert.Equal("und", res.VideoStream.Language);
+
+ Assert.Equal(MediaStreamType.Audio, res.MediaStreams[1].Type);
+ Assert.Equal("aac", res.MediaStreams[1].Codec);
+ Assert.Equal(7, res.MediaStreams[1].Channels);
+ Assert.True(res.MediaStreams[1].IsDefault);
+ Assert.Equal("eng", res.MediaStreams[1].Language);
+ Assert.Equal("Surround 6.1", res.MediaStreams[1].Title);
+
+ Assert.Equal(MediaStreamType.Audio, res.MediaStreams[2].Type);
+ Assert.Equal("aac", res.MediaStreams[2].Codec);
+ Assert.Equal(2, res.MediaStreams[2].Channels);
+ Assert.False(res.MediaStreams[2].IsDefault);
+ Assert.Equal("eng", res.MediaStreams[2].Language);
+ Assert.Equal("Commentary", res.MediaStreams[2].Title);
+
+ Assert.Equal("spa", res.MediaStreams[3].Language);
+ Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[3].Type);
+ Assert.Equal("DVDSUB", res.MediaStreams[3].Codec);
+ Assert.Null(res.MediaStreams[3].Title);
+
+ Assert.Equal("eng", res.MediaStreams[4].Language);
+ Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[4].Type);
+ Assert.Equal("mov_text", res.MediaStreams[4].Codec);
+ Assert.Null(res.MediaStreams[4].Title);
+
+ Assert.Equal("eng", res.MediaStreams[5].Language);
+ Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[5].Type);
+ Assert.Equal("mov_text", res.MediaStreams[5].Codec);
+ Assert.Equal("Commentary", res.MediaStreams[5].Title);
+ }
+
+ [Fact]
public void GetMediaInfo_MusicVideo_Success()
{
var bytes = File.ReadAllBytes("Test Data/Probing/music_video_metadata.json");
@@ -69,7 +137,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("Album", res.Album);
Assert.Equal(2021, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
- Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
}
[Fact]
@@ -85,7 +153,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("City to City", res.Album);
Assert.Equal(1978, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
- Assert.Equal(DateTime.Parse("1978-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ Assert.Equal(DateTime.Parse("1978-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
Assert.Contains("Electronic", res.Genres);
Assert.Contains("Ambient", res.Genres);
Assert.Contains("Pop", res.Genres);
@@ -105,7 +173,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("Eyes wide open", res.Album);
Assert.Equal(2020, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
- Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
Assert.Equal(22, res.People.Length);
Assert.Equal("Krysta Youngs", res.People[0].Name);
Assert.Equal(PersonType.Composer, res.People[0].Type);
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
index 537a944b0..c07c9ea7d 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -31,5 +31,27 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Assert.Equal("Very good, Lieutenant.", trackEvent2.Text);
}
}
+
+ [Fact]
+ public void Parse_EmptyNewlineBetweenText_Success()
+ {
+ using (var stream = File.OpenRead("Test Data/example2.srt"))
+ {
+ var parsed = new SrtParser(new NullLogger<SrtParser>()).Parse(stream, CancellationToken.None);
+ Assert.Equal(2, parsed.TrackEvents.Count);
+
+ var trackEvent1 = parsed.TrackEvents[0];
+ Assert.Equal("311", trackEvent1.Id);
+ Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
+ Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text);
+
+ var trackEvent2 = parsed.TrackEvents[1];
+ Assert.Equal("312", trackEvent2.Id);
+ Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
+ Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text);
+ }
+ }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
index 5db80c300..56649db8f 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
@@ -38,10 +38,11 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
}
}
- public static IEnumerable<object[]> Parse_MultipleDialogues_TestData()
+ public static TheoryData<string, IReadOnlyList<SubtitleTrackEvent>> Parse_MultipleDialogues_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, IReadOnlyList<SubtitleTrackEvent>>();
+
+ data.Add(
@"[Events]
Format: Layer, Start, End, Text
Dialogue: ,0:00:01.18,0:00:01.85,dialogue1
@@ -65,8 +66,9 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
StartPositionTicks = 31800000,
EndPositionTicks = 38500000
}
- }
- };
+ });
+
+ return data;
}
[Fact]
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
new file mode 100644
index 000000000..639c364df
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
@@ -0,0 +1,83 @@
+using System.Threading;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Subtitles.Tests
+{
+ public class SubtitleEncoderTests
+ {
+ internal static TheoryData<MediaSourceInfo, MediaStream, SubtitleEncoder.SubtitleInfo> GetReadableFile_Valid_TestData()
+ {
+ var data = new TheoryData<MediaSourceInfo, MediaStream, SubtitleEncoder.SubtitleInfo>();
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ass",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ssa",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ssa", MediaProtocol.File, "ssa", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.srt",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.srt", MediaProtocol.File, "srt", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.Http
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ass",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetReadableFile_Valid_TestData))]
+ internal async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo)
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ var subtitleEncoder = fixture.Create<SubtitleEncoder>();
+ var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(subtitleInfo.Path, result.Path);
+ Assert.Equal(subtitleInfo.Protocol, result.Protocol);
+ Assert.Equal(subtitleInfo.Format, result.Format);
+ Assert.Equal(subtitleInfo.IsExternal, result.IsExternal);
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_mp4_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_mp4_metadata.json
new file mode 100644
index 000000000..77e3def76
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_mp4_metadata.json
@@ -0,0 +1,260 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "High",
+ "codec_type": "video",
+ "codec_tag_string": "avc1",
+ "codec_tag": "0x31637661",
+ "width": 720,
+ "height": 358,
+ "coded_width": 720,
+ "coded_height": 358,
+ "closed_captions": 0,
+ "has_b_frames": 2,
+ "sample_aspect_ratio": "32:27",
+ "display_aspect_ratio": "1280:537",
+ "pix_fmt": "yuv420p",
+ "level": 31,
+ "color_range": "tv",
+ "color_space": "smpte170m",
+ "color_transfer": "bt709",
+ "color_primaries": "smpte170m",
+ "chroma_location": "left",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "r_frame_rate": "120/1",
+ "avg_frame_rate": "1704753000/71073479",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 1421469580,
+ "duration": "15794.106444",
+ "bit_rate": "1147365",
+ "bits_per_raw_sample": "8",
+ "nb_frames": "378834",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "und",
+ "handler_name": "VideoHandler",
+ "vendor_id": "[0][0][0][0]"
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_tag_string": "mp4a",
+ "codec_tag": "0x6134706d",
+ "sample_fmt": "fltp",
+ "sample_rate": "48000",
+ "channels": 7,
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/48000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 758115312,
+ "duration": "15794.069000",
+ "bit_rate": "224197",
+ "nb_frames": "740348",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "eng",
+ "handler_name": "Surround 6.1",
+ "vendor_id": "[0][0][0][0]"
+ }
+ },
+ {
+ "index": 2,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_tag_string": "mp4a",
+ "codec_tag": "0x6134706d",
+ "sample_fmt": "fltp",
+ "sample_rate": "48000",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/48000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 758114304,
+ "duration": "15794.048000",
+ "bit_rate": "160519",
+ "nb_frames": "740347",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "eng",
+ "handler_name": "Commentary",
+ "vendor_id": "[0][0][0][0]"
+ }
+ },
+ {
+ "index": 3,
+ "codec_name": "dvd_subtitle",
+ "codec_long_name": "DVD subtitles",
+ "codec_type": "subtitle",
+ "codec_tag_string": "mp4s",
+ "codec_tag": "0x7334706d",
+ "width": 720,
+ "height": 480,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 1300301588,
+ "duration": "14447.795422",
+ "bit_rate": "2653",
+ "nb_frames": "3545",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "spa",
+ "handler_name": "SubtitleHandler"
+ }
+ },
+ {
+ "index": 4,
+ "codec_name": "mov_text",
+ "codec_long_name": "MOV text",
+ "codec_type": "subtitle",
+ "codec_tag_string": "tx3g",
+ "codec_tag": "0x67337874",
+ "width": 853,
+ "height": 51,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 1401339330,
+ "duration": "15570.437000",
+ "bit_rate": "88",
+ "nb_frames": "5079",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "eng",
+ "handler_name": "SubtitleHandler"
+ }
+ },
+ {
+ "index": 5,
+ "codec_name": "mov_text",
+ "codec_long_name": "MOV text",
+ "codec_type": "subtitle",
+ "codec_tag_string": "tx3g",
+ "codec_tag": "0x67337874",
+ "width": 853,
+ "height": 51,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 1370580300,
+ "duration": "15228.670000",
+ "bit_rate": "18",
+ "nb_frames": "1563",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "eng",
+ "handler_name": "Commentary"
+ }
+ }
+ ]
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt
new file mode 100644
index 000000000..b14aa8ea3
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt
@@ -0,0 +1,11 @@
+311
+00:16:46,465 --> 00:16:49,009
+Una vez que la gente se entere
+
+de que ustedes están aquí,
+
+312
+00:16:49,092 --> 00:16:51,470
+este lugar se convertirá
+
+en un maldito zoológico.
diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
index ce9ecea6a..0c97a90b4 100644
--- a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
+++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using MediaBrowser.Model.Entities;
using Xunit;
@@ -6,12 +5,11 @@ namespace Jellyfin.Model.Tests.Entities
{
public class MediaStreamTests
{
- public static IEnumerable<object[]> Get_DisplayTitle_TestData()
+ public static TheoryData<MediaStream, string> Get_DisplayTitle_TestData()
{
- return new List<object[]>
- {
- new object[]
- {
+ var data = new TheoryData<MediaStream, string>();
+
+ data.Add(
new MediaStream
{
Type = MediaStreamType.Subtitle,
@@ -21,61 +19,57 @@ namespace Jellyfin.Model.Tests.Entities
IsDefault = false,
Codec = "ASS"
},
- "English - Und - ASS"
- },
- new object[]
+ "English - Und - ASS");
+
+ data.Add(
+ new MediaStream
{
- new MediaStream
- {
- Type = MediaStreamType.Subtitle,
- Title = "English",
- Language = string.Empty,
- IsForced = false,
- IsDefault = false,
- Codec = string.Empty
- },
- "English - Und"
+ Type = MediaStreamType.Subtitle,
+ Title = "English",
+ Language = string.Empty,
+ IsForced = false,
+ IsDefault = false,
+ Codec = string.Empty
},
- new object[]
+ "English - Und");
+
+ data.Add(
+ new MediaStream
{
- new MediaStream
- {
- Type = MediaStreamType.Subtitle,
- Title = "English",
- Language = "EN",
- IsForced = false,
- IsDefault = false,
- Codec = string.Empty
- },
- "English"
+ Type = MediaStreamType.Subtitle,
+ Title = "English",
+ Language = "EN",
+ IsForced = false,
+ IsDefault = false,
+ Codec = string.Empty
},
- new object[]
+ "English");
+
+ data.Add(
+ new MediaStream
{
- new MediaStream
- {
- Type = MediaStreamType.Subtitle,
- Title = "English",
- Language = "EN",
- IsForced = true,
- IsDefault = true,
- Codec = "SRT"
- },
- "English - Default - Forced - SRT"
+ Type = MediaStreamType.Subtitle,
+ Title = "English",
+ Language = "EN",
+ IsForced = true,
+ IsDefault = true,
+ Codec = "SRT"
},
- new object[]
+ "English - Default - Forced - SRT");
+
+ data.Add(
+ new MediaStream
{
- new MediaStream
- {
- Type = MediaStreamType.Subtitle,
- Title = null,
- Language = null,
- IsForced = false,
- IsDefault = false,
- Codec = null
- },
- "Und"
- }
- };
+ Type = MediaStreamType.Subtitle,
+ Title = null,
+ Language = null,
+ IsForced = false,
+ IsDefault = false,
+ Codec = null
+ },
+ "Und");
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 09b8a7a94..a37e5ac92 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index 53b35c2d6..c72a3315e 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using Xunit;
@@ -9,29 +8,29 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
private readonly NamingOptions _namingOptions = new NamingOptions();
- public static IEnumerable<object[]> Resolve_ValidFileNameTestData()
+ public static TheoryData<AudioBookFileInfo> Resolve_ValidFileNameTestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<AudioBookFileInfo>();
+
+ data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
- "mp3")
- };
- yield return new object[]
- {
+ "mp3"));
+
+ data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
"ogg",
- chapterNumber: 1)
- };
- yield return new object[]
- {
+ chapterNumber: 1));
+
+ data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
"mp3",
chapterNumber: 2,
- partNumber: 3)
- };
+ partNumber: 3));
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index a4ebab141..75d466198 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index f872f94f8..d13e89cee 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -18,30 +18,31 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestKodiExtras()
{
- Test("trailer.mp4", ExtraType.Trailer, _videoOptions);
- Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions);
+ Test("trailer.mp4", ExtraType.Trailer);
+ Test("300-trailer.mp4", ExtraType.Trailer);
- Test("theme.mp3", ExtraType.ThemeSong, _videoOptions);
+ Test("theme.mp3", ExtraType.ThemeSong);
}
[Fact]
public void TestExpandedExtras()
{
- Test("trailer.mp4", ExtraType.Trailer, _videoOptions);
- Test("trailer.mp3", null, _videoOptions);
- Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions);
-
- Test("theme.mp3", ExtraType.ThemeSong, _videoOptions);
- Test("theme.mkv", null, _videoOptions);
-
- Test("300-scene.mp4", ExtraType.Scene, _videoOptions);
- Test("300-scene2.mp4", ExtraType.Scene, _videoOptions);
- Test("300-clip.mp4", ExtraType.Clip, _videoOptions);
-
- Test("300-deleted.mp4", ExtraType.DeletedScene, _videoOptions);
- Test("300-deletedscene.mp4", ExtraType.DeletedScene, _videoOptions);
- Test("300-interview.mp4", ExtraType.Interview, _videoOptions);
- Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, _videoOptions);
+ Test("trailer.mp4", ExtraType.Trailer);
+ Test("trailer.mp3", null);
+ Test("300-trailer.mp4", ExtraType.Trailer);
+ Test("stuff trailerthings.mkv", null);
+
+ Test("theme.mp3", ExtraType.ThemeSong);
+ Test("theme.mkv", null);
+
+ Test("300-scene.mp4", ExtraType.Scene);
+ Test("300-scene2.mp4", ExtraType.Scene);
+ Test("300-clip.mp4", ExtraType.Clip);
+
+ Test("300-deleted.mp4", ExtraType.DeletedScene);
+ Test("300-deletedscene.mp4", ExtraType.DeletedScene);
+ Test("300-interview.mp4", ExtraType.Interview);
+ Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes);
}
[Theory]
@@ -55,9 +56,9 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData(ExtraType.Unknown, "extras")]
public void TestDirectories(ExtraType type, string dirName)
{
- Test(dirName + "/300.mp4", type, _videoOptions);
- Test("300/" + dirName + "/something.mkv", type, _videoOptions);
- Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type, _videoOptions);
+ Test(dirName + "/300.mp4", type);
+ Test("300/" + dirName + "/something.mkv", type);
+ Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type);
}
[Theory]
@@ -66,32 +67,25 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("The Big Short")]
public void TestNonExtraDirectories(string dirName)
{
- Test(dirName + "/300.mp4", null, _videoOptions);
- Test("300/" + dirName + "/something.mkv", null, _videoOptions);
- Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null, _videoOptions);
- Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null, _videoOptions);
+ Test(dirName + "/300.mp4", null);
+ Test("300/" + dirName + "/something.mkv", null);
+ Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null);
+ Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null);
}
[Fact]
public void TestSample()
{
- Test("300-sample.mp4", ExtraType.Sample, _videoOptions);
+ Test("300-sample.mp4", ExtraType.Sample);
}
- private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions)
+ private void Test(string input, ExtraType? expectedType)
{
- var parser = GetExtraTypeParser(videoOptions);
+ var parser = GetExtraTypeParser(_videoOptions);
var extraType = parser.GetExtraInfo(input).ExtraType;
- if (expectedType == null)
- {
- Assert.Null(extraType);
- }
- else
- {
- Assert.Equal(expectedType, extraType);
- }
+ Assert.Equal(expectedType, extraType);
}
[Fact]
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index ac5a7a21e..33a99e107 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@@ -11,148 +10,134 @@ namespace Jellyfin.Naming.Tests.Video
{
private static NamingOptions _namingOptions = new NamingOptions();
- public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
+ public static TheoryData<VideoFileInfo> ResolveFile_ValidFileNameTestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<VideoFileInfo>();
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
container: "mkv",
- name: "7 Psychos")
- };
- yield return new object[]
- {
+ name: "7 Psychos"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
container: "mkv",
name: "3 days to kill",
- year: 2005)
- };
- yield return new object[]
- {
+ year: 2005));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/American Psycho/American.Psycho.mkv",
container: "mkv",
- name: "American.Psycho")
- };
- yield return new object[]
- {
+ name: "American.Psycho"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
container: "mkv",
name: "brave",
year: 2006,
is3D: true,
- format3D: "sbs")
- };
- yield return new object[]
- {
+ format3D: "sbs"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
container: "mkv",
name: "300",
- year: 2006)
- };
- yield return new object[]
- {
+ year: 2006));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
container: "mkv",
name: "300",
year: 2006,
is3D: true,
- format3D: "sbs")
- };
- yield return new object[]
- {
+ format3D: "sbs"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
container: "disc",
name: "brave",
year: 2006,
isStub: true,
- stubType: "bluray")
- };
- yield return new object[]
- {
+ stubType: "bluray"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
container: "disc",
name: "300",
year: 2006,
isStub: true,
- stubType: "bluray")
- };
- yield return new object[]
- {
+ stubType: "bluray"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
container: "disc",
name: "Brave",
year: 2006,
isStub: true,
- stubType: "bluray")
- };
- yield return new object[]
- {
+ stubType: "bluray"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
container: "disc",
name: "300",
year: 2006,
isStub: true,
- stubType: "bluray")
- };
- yield return new object[]
- {
+ stubType: "bluray"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
container: "mkv",
name: "300",
year: 2006,
- extraType: ExtraType.Trailer)
- };
- yield return new object[]
- {
+ extraType: ExtraType.Trailer));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
container: "mkv",
name: "Brave",
year: 2006,
- extraType: ExtraType.Trailer)
- };
- yield return new object[]
- {
+ extraType: ExtraType.Trailer));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).mkv",
container: "mkv",
name: "300",
- year: 2006)
- };
- yield return new object[]
- {
+ year: 2006));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
container: "mkv",
name: "Bad Boys",
- year: 1995)
- };
- yield return new object[]
- {
+ year: 1995));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006).mkv",
container: "mkv",
name: "Brave",
- year: 2006)
- };
- yield return new object[]
- {
+ year: 2006));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4",
container: "mp4",
name: "Rain Man",
- year: 1988)
- };
+ year: 1988));
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 5fa2ecfe9..75d9b9ea9 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 97c14d463..a24eee693 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Networking.Tests
CallBase = true
};
configManager.Setup(x => x.GetConfiguration(It.IsAny<string>())).Returns(conf);
- return (IConfigurationManager)configManager.Object;
+ return configManager.Object;
}
/// <summary>
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index d9e33617b..0b2db64b0 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
new file mode 100644
index 000000000..b194e3885
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
@@ -0,0 +1,216 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Providers.MediaInfo;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo
+{
+ public class EmbeddedImageProviderTests
+ {
+ private static TheoryData<BaseItem> GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData()
+ {
+ return new ()
+ {
+ new AudioBook(),
+ new BoxSet(),
+ new Series(),
+ new Season(),
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData))]
+ public void GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty(BaseItem item)
+ {
+ var embeddedImageProvider = GetEmbeddedImageProvider(null);
+ Assert.Empty(embeddedImageProvider.GetSupportedImages(item));
+ }
+
+ private static TheoryData<BaseItem, IEnumerable<ImageType>> GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData()
+ {
+ return new TheoryData<BaseItem, IEnumerable<ImageType>>
+ {
+ { new Episode(), new List<ImageType> { ImageType.Primary } },
+ { new Movie(), new List<ImageType> { ImageType.Logo, ImageType.Backdrop, ImageType.Primary } },
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData))]
+ public void GetSupportedImages_SupportedBaseItems_ReturnsPopulated(BaseItem item, IEnumerable<ImageType> expected)
+ {
+ var embeddedImageProvider = GetEmbeddedImageProvider(null);
+ var actual = embeddedImageProvider.GetSupportedImages(item);
+ Assert.Equal(expected.OrderBy(i => i.ToString()), actual.OrderBy(i => i.ToString()));
+ }
+
+ [Fact]
+ public async void GetImage_InputWithNoStreams_ReturnsNoImage()
+ {
+ var embeddedImageProvider = GetEmbeddedImageProvider(null);
+
+ var input = GetMovie(new List<MediaAttachment>(), new List<MediaStream>());
+
+ var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.False(actual.HasImage);
+ }
+
+ [Fact]
+ public async void GetImage_InputWithUnlabeledAttachments_ReturnsNoImage()
+ {
+ var embeddedImageProvider = GetEmbeddedImageProvider(null);
+
+ // add an attachment without a filename - has a list to look through but finds nothing
+ var input = GetMovie(
+ new List<MediaAttachment> { new () },
+ new List<MediaStream>());
+
+ var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.False(actual.HasImage);
+ }
+
+ [Fact]
+ public async void GetImage_InputWithLabeledAttachments_ReturnsCorrectSelection()
+ {
+ // first tests file extension detection, second uses mimetype, third defaults to jpg
+ MediaAttachment sampleAttachment1 = new () { FileName = "clearlogo.png", Index = 1 };
+ MediaAttachment sampleAttachment2 = new () { FileName = "backdrop", MimeType = "image/bmp", Index = 2 };
+ MediaAttachment sampleAttachment3 = new () { FileName = "poster", Index = 3 };
+ string targetPath1 = "path1.png";
+ string targetPath2 = "path2.bmp";
+ string targetPath3 = "path2.jpg";
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 1, ".png", CancellationToken.None))
+ .Returns(Task.FromResult(targetPath1));
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 2, ".bmp", CancellationToken.None))
+ .Returns(Task.FromResult(targetPath2));
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 3, ".jpg", CancellationToken.None))
+ .Returns(Task.FromResult(targetPath3));
+ var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
+
+ var input = GetMovie(
+ new List<MediaAttachment> { sampleAttachment1, sampleAttachment2, sampleAttachment3 },
+ new List<MediaStream>());
+
+ var actualLogo = await embeddedImageProvider.GetImage(input, ImageType.Logo, CancellationToken.None);
+ Assert.NotNull(actualLogo);
+ Assert.True(actualLogo.HasImage);
+ Assert.Equal(targetPath1, actualLogo.Path);
+ Assert.Equal(ImageFormat.Png, actualLogo.Format);
+
+ var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
+ Assert.NotNull(actualBackdrop);
+ Assert.True(actualBackdrop.HasImage);
+ Assert.Equal(targetPath2, actualBackdrop.Path);
+ Assert.Equal(ImageFormat.Bmp, actualBackdrop.Format);
+
+ var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actualPrimary);
+ Assert.True(actualPrimary.HasImage);
+ Assert.Equal(targetPath3, actualPrimary.Path);
+ Assert.Equal(ImageFormat.Jpg, actualPrimary.Format);
+ }
+
+ [Fact]
+ public async void GetImage_InputWithUnlabeledEmbeddedImages_BackdropReturnsNoImage()
+ {
+ var embeddedImageProvider = GetEmbeddedImageProvider(null);
+
+ var input = GetMovie(
+ new List<MediaAttachment>(),
+ new List<MediaStream> { new () { Type = MediaStreamType.EmbeddedImage } });
+
+ var actual = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.False(actual.HasImage);
+ }
+
+ [Fact]
+ public async void GetImage_InputWithUnlabeledEmbeddedImages_PrimaryReturnsImage()
+ {
+ MediaStream sampleStream = new () { Type = MediaStreamType.EmbeddedImage, Index = 1 };
+ string targetPath = "path";
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream, 1, ".jpg", CancellationToken.None))
+ .Returns(Task.FromResult(targetPath));
+ var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
+
+ var input = GetMovie(
+ new List<MediaAttachment>(),
+ new List<MediaStream> { sampleStream });
+
+ var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.True(actual.HasImage);
+ Assert.Equal(targetPath, actual.Path);
+ Assert.Equal(ImageFormat.Jpg, actual.Format);
+ }
+
+ [Fact]
+ public async void GetImage_InputWithLabeledEmbeddedImages_ReturnsCorrectSelection()
+ {
+ // primary is second stream to ensure it's not defaulting, backdrop is first
+ MediaStream sampleStream1 = new () { Type = MediaStreamType.EmbeddedImage, Index = 1, Comment = "backdrop" };
+ MediaStream sampleStream2 = new () { Type = MediaStreamType.EmbeddedImage, Index = 2, Comment = "cover" };
+ string targetPath1 = "path1.jpg";
+ string targetPath2 = "path2.jpg";
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream1, 1, ".jpg", CancellationToken.None))
+ .Returns(Task.FromResult(targetPath1));
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream2, 2, ".jpg", CancellationToken.None))
+ .Returns(Task.FromResult(targetPath2));
+ var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
+
+ var input = GetMovie(
+ new List<MediaAttachment>(),
+ new List<MediaStream> { sampleStream1, sampleStream2 });
+
+ var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actualPrimary);
+ Assert.True(actualPrimary.HasImage);
+ Assert.Equal(targetPath2, actualPrimary.Path);
+ Assert.Equal(ImageFormat.Jpg, actualPrimary.Format);
+
+ var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
+ Assert.NotNull(actualBackdrop);
+ Assert.True(actualBackdrop.HasImage);
+ Assert.Equal(targetPath1, actualBackdrop.Path);
+ Assert.Equal(ImageFormat.Jpg, actualBackdrop.Format);
+ }
+
+ private static EmbeddedImageProvider GetEmbeddedImageProvider(IMediaEncoder? mediaEncoder)
+ {
+ return new EmbeddedImageProvider(mediaEncoder);
+ }
+
+ private static Movie GetMovie(List<MediaAttachment> mediaAttachments, List<MediaStream> mediaStreams)
+ {
+ // Mocking IMediaSourceManager GetMediaAttachments and GetMediaStreams instead of mocking Movie works, but
+ // has concurrency problems between this and VideoImageProviderTests due to BaseItem.MediaSourceManager
+ // being static
+ var movie = new Mock<Movie>();
+
+ movie.Setup(item => item.GetMediaSources(It.IsAny<bool>()))
+ .Returns(new List<MediaSourceInfo> { new () { MediaAttachments = mediaAttachments } } );
+ movie.Setup(item => item.GetMediaStreams())
+ .Returns(mediaStreams);
+
+ return movie.Object;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
index b160e676e..c289a7112 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CA1002 // Do not expose generic lists
+#pragma warning disable CA1002 // Do not expose generic lists
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
@@ -11,11 +11,12 @@ namespace Jellyfin.Providers.Tests.MediaInfo
{
public class SubtitleResolverTests
{
- public static IEnumerable<object[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
+ public static TheoryData<List<MediaStream>, string, int, string[], MediaStream[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
{
+ var data = new TheoryData<List<MediaStream>, string, int, string[], MediaStream[]>();
+
var index = 0;
- yield return new object[]
- {
+ data.Add(
new List<MediaStream>(),
"/video/My.Video.mkv",
index,
@@ -52,8 +53,9 @@ namespace Jellyfin.Providers.Tests.MediaInfo
CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true),
CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true),
CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index),
- }
- };
+ });
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs
new file mode 100644
index 000000000..0f51a2b8f
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Providers.MediaInfo;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo
+{
+ public class VideoImageProviderTests
+ {
+ [Fact]
+ public async void GetImage_InputIsPlaceholder_ReturnsNoImage()
+ {
+ var videoImageProvider = GetVideoImageProvider(null);
+
+ var input = new Movie
+ {
+ IsPlaceHolder = true
+ };
+
+ var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.False(actual.HasImage);
+ }
+
+ [Fact]
+ public async void GetImage_NoDefaultVideoStream_ReturnsNoImage()
+ {
+ var videoImageProvider = GetVideoImageProvider(null);
+
+ var input = new Movie
+ {
+ DefaultVideoStreamIndex = null
+ };
+
+ var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.False(actual.HasImage);
+ }
+
+ [Fact]
+ public async void GetImage_DefaultSetButNoVideoStream_ReturnsNoImage()
+ {
+ var videoImageProvider = GetVideoImageProvider(null);
+
+ // set a default index but don't put anything there (invalid input, but provider shouldn't break)
+ var input = GetMovie(0, null, new List<MediaStream>());
+
+ var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.False(actual.HasImage);
+ }
+
+ [Fact]
+ public async void GetImage_DefaultSetMultipleVideoStreams_ReturnsDefaultStreamImage()
+ {
+ MediaStream firstStream = new () { Type = MediaStreamType.Video, Index = 0 };
+ MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 1 };
+ string targetPath = "path.jpg";
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), firstStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
+ .Returns(Task.FromResult("wrong stream called!"));
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), targetStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
+ .Returns(Task.FromResult(targetPath));
+ var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
+
+ var input = GetMovie(1, targetStream, new List<MediaStream> { firstStream, targetStream } );
+
+ var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.True(actual.HasImage);
+ Assert.Equal(targetPath, actual.Path);
+ Assert.Equal(ImageFormat.Jpg, actual.Format);
+ }
+
+ [Fact]
+ public async void GetImage_InvalidDefaultSingleVideoStream_ReturnsFirstVideoStreamImage()
+ {
+ MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
+ string targetPath = "path.jpg";
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), targetStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
+ .Returns(Task.FromResult(targetPath));
+ var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
+
+ // provide query results for default (empty) and all streams (populated)
+ var input = GetMovie(5, null, new List<MediaStream> { targetStream });
+
+ var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.True(actual.HasImage);
+ Assert.Equal(targetPath, actual.Path);
+ Assert.Equal(ImageFormat.Jpg, actual.Format);
+ }
+
+ [Fact]
+ public async void GetImage_NoTimeSpanSet_CallsEncoderWithDefaultTime()
+ {
+ MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
+
+ // use a callback to catch the actual value
+ // provides more information on failure than verifying a specific input was called on the mock
+ TimeSpan? actualTimeSpan = null;
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
+ .Callback<string, string, MediaSourceInfo, MediaStream, Video3DFormat?, TimeSpan?, CancellationToken>((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan)
+ .Returns(Task.FromResult("path"));
+ var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
+
+ var input = GetMovie(0, targetStream, new List<MediaStream> { targetStream });
+
+ // not testing return, just verifying what gets requested for time span
+ await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+
+ Assert.Equal(TimeSpan.FromSeconds(10), actualTimeSpan);
+ }
+
+ [Fact]
+ public async void GetImage_TimeSpanSet_CallsEncoderWithCalculatedTime()
+ {
+ MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
+
+ TimeSpan? actualTimeSpan = null;
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
+ .Callback<string, string, MediaSourceInfo, MediaStream, Video3DFormat?, TimeSpan?, CancellationToken>((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan)
+ .Returns(Task.FromResult("path"));
+ var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
+
+ var input = GetMovie(0, targetStream, new List<MediaStream> { targetStream });
+ input.RunTimeTicks = 5000;
+
+ // not testing return, just verifying what gets requested for time span
+ await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+
+ Assert.Equal(TimeSpan.FromTicks(500), actualTimeSpan);
+ }
+
+ private static VideoImageProvider GetVideoImageProvider(IMediaEncoder? mediaEncoder)
+ {
+ // strict to ensure this isn't accidentally used where a prepared mock is intended
+ mediaEncoder ??= new Mock<IMediaEncoder>(MockBehavior.Strict).Object;
+ return new VideoImageProvider(mediaEncoder, new NullLogger<VideoImageProvider>());
+ }
+
+ private static Movie GetMovie(int defaultVideoStreamIndex, MediaStream? defaultStream, List<MediaStream> mediaStreams)
+ {
+ // Mocking IMediaSourceManager GetMediaStreams instead of mocking Movie works, but has concurrency problems
+ // between this and EmbeddedImageProviderTests due to BaseItem.MediaSourceManager being static
+ var movie = new Mock<Movie>
+ {
+ Object =
+ {
+ DefaultVideoStreamIndex = defaultVideoStreamIndex
+ }
+ };
+
+ movie.Setup(item => item.GetDefaultVideoStream())
+ .Returns(defaultStream!);
+ movie.Setup(item => item.GetMediaStreams())
+ .Returns(mediaStreams);
+
+ return movie.Object;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
index a6e1dfe8f..6337dea41 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
@@ -32,10 +32,11 @@ namespace Jellyfin.Server.Implementations.Tests.Data
_sqliteItemRepository = _fixture.Create<SqliteItemRepository>();
}
- public static IEnumerable<object[]> ItemImageInfoFromValueString_Valid_TestData()
+ public static TheoryData<string, ItemImageInfo> ItemImageInfoFromValueString_Valid_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, ItemImageInfo>();
+
+ data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
new ItemImageInfo
{
@@ -45,41 +46,33 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Width = 1920,
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*0*0",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*600",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"%MetadataPath%/library/68/68578562b96c80a7ebd530848801f645/poster.jpg*637264380567586027*Primary*600*336",
new ItemImageInfo
{
@@ -88,8 +81,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
DateModified = new DateTime(637264380567586027, DateTimeKind.Utc),
Width = 600,
Height = 336
- }
- };
+ });
+
+ return data;
}
[Theory]
@@ -117,10 +111,10 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));
}
- public static IEnumerable<object[]> DeserializeImages_Valid_TestData()
+ public static TheoryData<string, ItemImageInfo[]> DeserializeImages_Valid_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, ItemImageInfo[]>();
+ data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
new ItemImageInfo[]
{
@@ -133,11 +127,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg*637261226720645297*Primary*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png*637261226720805297*Logo*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg*637261226721285297*Thumb*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg*637261226721685297*Backdrop*0*0",
new ItemImageInfo[]
{
@@ -165,20 +157,19 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Type = ImageType.Backdrop,
DateModified = new DateTime(637261226721685297, DateTimeKind.Utc),
}
- }
- };
+ });
+
+ return data;
}
- public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData()
+ public static TheoryData<string, ItemImageInfo[]> DeserializeImages_ValidAndInvalid_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, ItemImageInfo[]>();
+ data.Add(
string.Empty,
- Array.Empty<ItemImageInfo>()
- };
+ Array.Empty<ItemImageInfo>());
- yield return new object[]
- {
+ data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss",
new ItemImageInfo[]
{
@@ -191,14 +182,13 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"|",
- Array.Empty<ItemImageInfo>()
- };
+ Array.Empty<ItemImageInfo>());
+
+ return data;
}
[Theory]
@@ -242,30 +232,27 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value));
}
- public static IEnumerable<object[]> DeserializeProviderIds_Valid_TestData()
+ public static TheoryData<string, Dictionary<string, string>> DeserializeProviderIds_Valid_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, Dictionary<string, string>>();
+
+ data.Add(
"Imdb=tt0119567",
new Dictionary<string, string>()
{
{ "Imdb", "tt0119567" },
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"Imdb=tt0119567|Tmdb=330|TmdbCollection=328",
new Dictionary<string, string>()
{
{ "Imdb", "tt0119567" },
{ "Tmdb", "330" },
{ "TmdbCollection", "328" },
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"MusicBrainzAlbum=9d363e43-f24f-4b39-bc5a-7ef305c677c7|MusicBrainzReleaseGroup=63eba062-847c-3b73-8b0f-6baf27bba6fa|AudioDbArtist=111352|AudioDbAlbum=2116560|MusicBrainzAlbumArtist=20244d07-534f-4eff-b4d4-930878889970",
new Dictionary<string, string>()
{
@@ -274,8 +261,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
{ "AudioDbArtist", "111352" },
{ "AudioDbAlbum", "2116560" },
{ "MusicBrainzAlbumArtist", "20244d07-534f-4eff-b4d4-930878889970" },
- }
- };
+ });
+
+ return data;
}
[Theory]
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 9b6ab7bdf..5ecd84604 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
<RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
index e8b93b437..976afe195 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using Emby.Server.Implementations.LiveTv.EmbyTV;
using MediaBrowser.Controller.LiveTv;
using Xunit;
@@ -8,43 +7,36 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
public static class RecordingHelperTests
{
- public static IEnumerable<object[]> GetRecordingName_Success_TestData()
+ public static TheoryData<string, TimerInfo> GetRecordingName_Success_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, TimerInfo>();
+
+ data.Add(
"The Incredibles 2020_04_20_21_06_00",
new TimerInfo
{
Name = "The Incredibles",
StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
IsMovie = true
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"The Incredibles (2004)",
new TimerInfo
{
Name = "The Incredibles",
IsMovie = true,
ProductionYear = 2004
- }
- };
-
- yield return new object[]
- {
+ });
+ data.Add(
"The Big Bang Theory 2020_04_20_21_06_00",
new TimerInfo
{
Name = "The Big Bang Theory",
StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
IsProgramSeries = true,
- }
- };
-
- yield return new object[]
- {
+ });
+ data.Add(
"The Big Bang Theory S12E10",
new TimerInfo
{
@@ -52,11 +44,8 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
IsProgramSeries = true,
SeasonNumber = 12,
EpisodeNumber = 10
- }
- };
-
- yield return new object[]
- {
+ });
+ data.Add(
"The Big Bang Theory S12E10 The VCR Illumination",
new TimerInfo
{
@@ -65,22 +54,17 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
SeasonNumber = 12,
EpisodeNumber = 10,
EpisodeTitle = "The VCR Illumination"
- }
- };
-
- yield return new object[]
- {
+ });
+ data.Add(
"The Big Bang Theory 2018-12-06",
new TimerInfo
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6)
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"The Big Bang Theory 2018-12-06 - The VCR Illumination",
new TimerInfo
{
@@ -88,11 +72,9 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6),
EpisodeTitle = "The VCR Illumination"
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"The Big Bang Theory 2018_12_06_21_06_00 - The VCR Illumination",
new TimerInfo
{
@@ -101,8 +83,9 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6),
EpisodeTitle = "The VCR Illumination"
- }
- };
+ });
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
new file mode 100644
index 000000000..3b3e38bd1
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
+using Jellyfin.Extensions.Json;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
+{
+ public class SchedulesDirectDeserializeTests
+ {
+ private readonly JsonSerializerOptions _jsonOptions;
+
+ public SchedulesDirectDeserializeTests()
+ {
+ _jsonOptions = JsonDefaults.Options;
+ }
+
+ /// <summary>
+ /// /token reponse.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Token_Response_Live_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/token_live_response.json");
+ var tokenDto = JsonSerializer.Deserialize<TokenDto>(bytes, _jsonOptions);
+
+ Assert.NotNull(tokenDto);
+ Assert.Equal(0, tokenDto!.Code);
+ Assert.Equal("OK", tokenDto.Message);
+ Assert.Equal("AWS-SD-web.1", tokenDto.ServerId);
+ Assert.Equal(new DateTime(2016, 08, 23, 13, 55, 25, DateTimeKind.Utc), tokenDto.TokenTimestamp);
+ Assert.Equal("f3fca79989cafe7dead71beefedc812b", tokenDto.Token);
+ }
+
+ /// <summary>
+ /// /token response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Token_Response_Offline_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/token_offline_response.json");
+ var tokenDto = JsonSerializer.Deserialize<TokenDto>(bytes, _jsonOptions);
+
+ Assert.NotNull(tokenDto);
+ Assert.Equal(3_000, tokenDto!.Code);
+ Assert.Equal("Server offline for maintenance.", tokenDto.Message);
+ Assert.Equal("20141201.web.1", tokenDto.ServerId);
+ Assert.Equal(new DateTime(2015, 04, 23, 00, 03, 32, DateTimeKind.Utc), tokenDto.TokenTimestamp);
+ Assert.Equal("CAFEDEADBEEFCAFEDEADBEEFCAFEDEADBEEFCAFE", tokenDto.Token);
+ Assert.Equal("SERVICE_OFFLINE", tokenDto.Response);
+ }
+
+ /// <summary>
+ /// /schedules request.
+ /// </summary>
+ [Fact]
+ public void Serialize_Schedule_Request_Success()
+ {
+ var expectedString = File.ReadAllText("Test Data/SchedulesDirect/schedules_request.json").Trim();
+
+ var requestObject = new RequestScheduleForChannelDto[]
+ {
+ new RequestScheduleForChannelDto
+ {
+ StationId = "20454",
+ Date = new[]
+ {
+ "2015-03-13",
+ "2015-03-17"
+ }
+ },
+ new RequestScheduleForChannelDto
+ {
+ StationId = "10021",
+ Date = new[]
+ {
+ "2015-03-12",
+ "2015-03-13"
+ }
+ }
+ };
+
+ var requestString = JsonSerializer.Serialize(requestObject, _jsonOptions);
+ Assert.Equal(expectedString, requestString);
+ }
+
+ /// <summary>
+ /// /schedules response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Schedule_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/schedules_response.json");
+ var days = JsonSerializer.Deserialize<IReadOnlyList<DayDto>>(bytes, _jsonOptions);
+
+ Assert.NotNull(days);
+ Assert.Equal(1, days!.Count);
+
+ var dayDto = days[0];
+ Assert.Equal("20454", dayDto.StationId);
+ Assert.Equal(2, dayDto.Programs.Count);
+
+ Assert.Equal("SH005371070000", dayDto.Programs[0].ProgramId);
+ Assert.Equal(new DateTime(2015, 03, 03, 00, 00, 00, DateTimeKind.Utc), dayDto.Programs[0].AirDateTime);
+ Assert.Equal(1_800, dayDto.Programs[0].Duration);
+ Assert.Equal("Sy8HEMBPcuiAx3FBukUhKQ", dayDto.Programs[0].Md5);
+ Assert.True(dayDto.Programs[0].New);
+ Assert.Equal(2, dayDto.Programs[0].AudioProperties.Count);
+ Assert.Equal("stereo", dayDto.Programs[0].AudioProperties[0]);
+ Assert.Equal("cc", dayDto.Programs[0].AudioProperties[1]);
+ Assert.Equal(1, dayDto.Programs[0].VideoProperties.Count);
+ Assert.Equal("hdtv", dayDto.Programs[0].VideoProperties[0]);
+ }
+
+ /// <summary>
+ /// /programs response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Program_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/programs_response.json");
+ var programDtos = JsonSerializer.Deserialize<IReadOnlyList<ProgramDetailsDto>>(bytes, _jsonOptions);
+
+ Assert.NotNull(programDtos);
+ Assert.Equal(2, programDtos!.Count);
+ Assert.Equal("EP000000060003", programDtos[0].ProgramId);
+ Assert.Equal(1, programDtos[0].Titles.Count);
+ Assert.Equal("'Allo 'Allo!", programDtos[0].Titles[0].Title120);
+ Assert.Equal("Series", programDtos[0].EventDetails?.SubType);
+ Assert.Equal("en", programDtos[0].Descriptions?.Description1000[0].DescriptionLanguage);
+ Assert.Equal("A disguised British Intelligence officer is sent to help the airmen.", programDtos[0].Descriptions?.Description1000[0].Description);
+ Assert.Equal(new DateTime(1985, 11, 04), programDtos[0].OriginalAirDate);
+ Assert.Equal(1, programDtos[0].Genres.Count);
+ Assert.Equal("Sitcom", programDtos[0].Genres[0]);
+ Assert.Equal("The Poloceman Cometh", programDtos[0].EpisodeTitle150);
+ Assert.Equal(2, programDtos[0].Metadata[0].Gracenote?.Season);
+ Assert.Equal(3, programDtos[0].Metadata[0].Gracenote?.Episode);
+ Assert.Equal(13, programDtos[0].Cast.Count);
+ Assert.Equal("383774", programDtos[0].Cast[0].PersonId);
+ Assert.Equal("392649", programDtos[0].Cast[0].NameId);
+ Assert.Equal("Gorden Kaye", programDtos[0].Cast[0].Name);
+ Assert.Equal("Actor", programDtos[0].Cast[0].Role);
+ Assert.Equal("01", programDtos[0].Cast[0].BillingOrder);
+ Assert.Equal(3, programDtos[0].Crew.Count);
+ Assert.Equal("354407", programDtos[0].Crew[0].PersonId);
+ Assert.Equal("363281", programDtos[0].Crew[0].NameId);
+ Assert.Equal("David Croft", programDtos[0].Crew[0].Name);
+ Assert.Equal("Director", programDtos[0].Crew[0].Role);
+ Assert.Equal("01", programDtos[0].Crew[0].BillingOrder);
+ }
+
+ /// <summary>
+ /// /metadata/programs response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Metadata_Programs_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/metadata_programs_response.json");
+ var showImagesDtos = JsonSerializer.Deserialize<IReadOnlyList<ShowImagesDto>>(bytes, _jsonOptions);
+
+ Assert.NotNull(showImagesDtos);
+ Assert.Equal(1, showImagesDtos!.Count);
+ Assert.Equal("SH00712240", showImagesDtos[0].ProgramId);
+ Assert.Equal(4, showImagesDtos[0].Data.Count);
+ Assert.Equal("135", showImagesDtos[0].Data[0].Width);
+ Assert.Equal("180", showImagesDtos[0].Data[0].Height);
+ Assert.Equal("assets/p282288_b_v2_aa.jpg", showImagesDtos[0].Data[0].Uri);
+ Assert.Equal("Sm", showImagesDtos[0].Data[0].Size);
+ Assert.Equal("3x4", showImagesDtos[0].Data[0].Aspect);
+ Assert.Equal("Banner-L3", showImagesDtos[0].Data[0].Category);
+ Assert.Equal("yes", showImagesDtos[0].Data[0].Text);
+ Assert.Equal("true", showImagesDtos[0].Data[0].Primary);
+ Assert.Equal("Series", showImagesDtos[0].Data[0].Tier);
+ }
+
+ /// <summary>
+ /// /headends response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Headends_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/headends_response.json");
+ var headendsDtos = JsonSerializer.Deserialize<IReadOnlyList<HeadendsDto>>(bytes, _jsonOptions);
+
+ Assert.NotNull(headendsDtos);
+ Assert.Equal(8, headendsDtos!.Count);
+ Assert.Equal("CA00053", headendsDtos[0].Headend);
+ Assert.Equal("Cable", headendsDtos[0].Transport);
+ Assert.Equal("Beverly Hills", headendsDtos[0].Location);
+ Assert.Equal(2, headendsDtos[0].Lineups.Count);
+ Assert.Equal("Time Warner Cable - Cable", headendsDtos[0].Lineups[0].Name);
+ Assert.Equal("USA-CA00053-DEFAULT", headendsDtos[0].Lineups[0].Lineup);
+ Assert.Equal("/20141201/lineups/USA-CA00053-DEFAULT", headendsDtos[0].Lineups[0].Uri);
+ }
+
+ /// <summary>
+ /// /lineups response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Lineups_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/lineups_response.json");
+ var lineupsDto = JsonSerializer.Deserialize<LineupsDto>(bytes, _jsonOptions);
+
+ Assert.NotNull(lineupsDto);
+ Assert.Equal(0, lineupsDto!.Code);
+ Assert.Equal("20141201.web.1", lineupsDto.ServerId);
+ Assert.Equal(new DateTime(2015, 04, 17, 14, 22, 17, DateTimeKind.Utc), lineupsDto.LineupTimestamp);
+ Assert.Equal(5, lineupsDto.Lineups.Count);
+ Assert.Equal("GBR-0001317-DEFAULT", lineupsDto.Lineups[0].Lineup);
+ Assert.Equal("Freeview - Carlton - LWT (Southeast)", lineupsDto.Lineups[0].Name);
+ Assert.Equal("DVB-T", lineupsDto.Lineups[0].Transport);
+ Assert.Equal("London", lineupsDto.Lineups[0].Location);
+ Assert.Equal("/20141201/lineups/GBR-0001317-DEFAULT", lineupsDto.Lineups[0].Uri);
+
+ Assert.Equal("DELETED LINEUP", lineupsDto.Lineups[4].Name);
+ Assert.True(lineupsDto.Lineups[4].IsDeleted);
+ }
+
+ /// <summary>
+ /// /lineup/:id response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Lineup_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/lineup_response.json");
+ var channelDto = JsonSerializer.Deserialize<ChannelDto>(bytes, _jsonOptions);
+
+ Assert.NotNull(channelDto);
+ Assert.Equal(2, channelDto!.Map.Count);
+ Assert.Equal("24326", channelDto.Map[0].StationId);
+ Assert.Equal("001", channelDto.Map[0].Channel);
+ Assert.Equal("BBC ONE South", channelDto.Map[0].ProvderCallsign);
+ Assert.Equal("1", channelDto.Map[0].LogicalChannelNumber);
+ Assert.Equal("providerCallsign", channelDto.Map[0].MatchType);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs
index 043363ae3..28d832ef8 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.QuickConnect;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
@@ -51,6 +52,21 @@ namespace Jellyfin.Server.Implementations.Tests.QuickConnect
public void IsEnabled_QuickConnectUnavailable_False()
=> Assert.False(_quickConnectManager.IsEnabled);
+ [Theory]
+ [InlineData("", "DeviceId", "Client", "1.0.0")]
+ [InlineData("Device", "", "Client", "1.0.0")]
+ [InlineData("Device", "DeviceId", "", "1.0.0")]
+ [InlineData("Device", "DeviceId", "Client", "")]
+ public void TryConnect_InvalidAuthorizationInfo_ThrowsArgumentException(string device, string deviceId, string client, string version)
+ => Assert.Throws<ArgumentException>(() => _quickConnectManager.TryConnect(
+ new AuthorizationInfo
+ {
+ Device = device,
+ DeviceId = deviceId,
+ Client = client,
+ Version = version
+ }));
+
[Fact]
public void TryConnect_QuickConnectUnavailable_ThrowsAuthenticationException()
=> Assert.Throws<AuthenticationException>(() => _quickConnectManager.TryConnect(_quickConnectAuthInfo));
@@ -64,6 +80,10 @@ namespace Jellyfin.Server.Implementations.Tests.QuickConnect
=> Assert.ThrowsAsync<AuthenticationException>(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty));
[Fact]
+ public void GetAuthorizedRequest_QuickConnectUnavailable_ThrowsAuthenticationException()
+ => Assert.Throws<AuthenticationException>(() => _quickConnectManager.GetAuthorizedRequest(string.Empty));
+
+ [Fact]
public void IsEnabled_QuickConnectAvailable_True()
{
_config.QuickConnectAvailable = true;
@@ -80,6 +100,20 @@ namespace Jellyfin.Server.Implementations.Tests.QuickConnect
}
[Fact]
+ public void CheckRequestStatus_UnknownSecret_ThrowsResourceNotFoundException()
+ {
+ _config.QuickConnectAvailable = true;
+ Assert.Throws<ResourceNotFoundException>(() => _quickConnectManager.CheckRequestStatus("Unknown secret"));
+ }
+
+ [Fact]
+ public void GetAuthorizedRequest_UnknownSecret_ThrowsResourceNotFoundException()
+ {
+ _config.QuickConnectAvailable = true;
+ Assert.Throws<ResourceNotFoundException>(() => _quickConnectManager.GetAuthorizedRequest("Unknown secret"));
+ }
+
+ [Fact]
public async Task AuthorizeRequest_QuickConnectAvailable_Success()
{
_config.QuickConnectAvailable = true;
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
index d9b206f66..59d82678e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections;
-using System.Collections.Generic;
using Emby.Server.Implementations.Sorting;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -13,7 +11,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting
{
[Theory]
[ClassData(typeof(EpisodeBadData))]
- public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem x, BaseItem y)
+ public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y)
{
var cmp = new AiredEpisodeOrderComparer();
Assert.Throws<ArgumentNullException>(() => cmp.Compare(x, y));
@@ -29,152 +27,138 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting
Assert.Equal(-expected, cmp.Compare(y, x));
}
- private class EpisodeBadData : IEnumerable<object?[]>
+ private class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
{
- public IEnumerator<object?[]> GetEnumerator()
+ public EpisodeBadData()
{
- yield return new object?[] { null, new Episode() };
- yield return new object?[] { new Episode(), null };
+ Add(null, new Episode());
+ Add(new Episode(), null);
}
-
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
- private class EpisodeTestData : IEnumerable<object?[]>
+ private class EpisodeTestData : TheoryData<BaseItem, BaseItem, int>
{
- public IEnumerator<object?[]> GetEnumerator()
+ public EpisodeTestData()
{
- yield return new object?[]
- {
+ Add(
new Movie(),
new Movie(),
- 0
- };
- yield return new object?[]
- {
+ 0);
+
+ Add(
new Movie(),
new Episode(),
- 1
- };
+ 1);
+
// Good cases
- yield return new object?[]
- {
+ Add(
new Episode(),
new Episode(),
- 0
- };
- yield return new object?[]
- {
+ 0);
+
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
- 0
- };
- yield return new object?[]
- {
+ 0);
+
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
- 1
- };
- yield return new object?[]
- {
+ 1);
+
+ Add(
new Episode { ParentIndexNumber = 2, IndexNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
- 1
- };
+ 1);
+
// Good Specials
- yield return new object?[]
- {
+ Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
- 0
- };
- yield return new object?[]
- {
+ 0);
+
+ Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
- 1
- };
+ 1);
// Specials to Episodes
- yield return new object?[]
- {
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
- 1
- };
- yield return new object?[]
- {
+ 1);
+
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
- 1
- };
- yield return new object?[]
- {
+ 1);
+
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
- 1
- };
+ 1);
- yield return new object?[]
- {
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
- 1
- };
- yield return new object?[]
- {
+ 1);
+
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
- 1
- };
+ 1);
- yield return new object?[]
- {
+ Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
- 1
- };
- yield return new object?[]
- {
+ 1);
+
+ Add(
new Episode { ParentIndexNumber = 3, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1 },
- 1
- };
+ 1);
- yield return new object?[]
- {
+ Add(
new Episode { ParentIndexNumber = 3, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
- 1
- };
+ 1);
- yield return new object?[]
- {
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1 },
- 1
- };
- yield return new object?[]
- {
+ 1);
+
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
- 1
- };
- yield return new object?[]
- {
+ 1);
+
+ Add(
new Episode { ParentIndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
- 0
- };
- yield return new object?[]
- {
+ 0);
+
+ Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 3 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
- 1
- };
- }
+ 1);
+
+ // Premiere Date
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ 0);
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ -1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
+ 1);
+ }
}
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json
new file mode 100644
index 000000000..015afeecc
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json
@@ -0,0 +1 @@
+[{"headend":"CA00053","transport":"Cable","location":"Beverly Hills","lineups":[{"name":"Time Warner Cable - Cable","lineup":"USA-CA00053-DEFAULT","uri":"/20141201/lineups/USA-CA00053-DEFAULT"},{"name":"Time Warner Cable - Digital","lineup":"USA-CA00053-X","uri":"/20141201/lineups/USA-CA00053-X"}]},{"headend":"CA61222","transport":"Cable","location":"Beverly Hills","lineups":[{"name":"Mulholland Estates - Cable","lineup":"USA-CA61222-DEFAULT","uri":"/20141201/lineups/USA-CA61222-DEFAULT"}]},{"headend":"CA66511","transport":"Cable","location":"Los Angeles","lineups":[{"name":"AT&T U-verse TV - Digital","lineup":"USA-CA66511-X","uri":"/20141201/lineups/USA-CA66511-X"}]},{"headend":"CA67309","transport":"Cable","location":"Westchester","lineups":[{"name":"Time Warner Cable Sherman Oaks - Cable","lineup":"USA-CA67309-DEFAULT","uri":"/20141201/lineups/USA-CA67309-DEFAULT"},{"name":"Time Warner Cable Sherman Oaks - Digital","lineup":"USA-CA67309-X","uri":"/20141201/lineups/USA-CA67309-X"}]},{"headend":"CA67310","transport":"Cable","location":"Eagle Rock","lineups":[{"name":"Time Warner Cable City of Los Angeles - Cable","lineup":"USA-CA67310-DEFAULT","uri":"/20141201/lineups/USA-CA67310-DEFAULT"},{"name":"Time Warner Cable City of Los Angeles - Digital","lineup":"USA-CA67310-X","uri":"/20141201/lineups/USA-CA67310-X"}]},{"headend":"DISH803","transport":"Satellite","location":"Los Angeles","lineups":[{"name":"DISH Los Angeles - Satellite","lineup":"USA-DISH803-DEFAULT","uri":"/20141201/lineups/USA-DISH803-DEFAULT"}]},{"headend":"DITV803","transport":"Satellite","location":"Los Angeles","lineups":[{"name":"DIRECTV Los Angeles - Satellite","lineup":"USA-DITV803-DEFAULT","uri":"/20141201/lineups/USA-DITV803-DEFAULT"}]},{"headend":"90210","transport":"Antenna","location":"90210","lineups":[{"name":"Antenna","lineup":"USA-OTA-90210","uri":"/20141201/lineups/USA-OTA-90210"}]}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json
new file mode 100644
index 000000000..072089470
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json
@@ -0,0 +1 @@
+{"map":[{"stationID":"24326","channel":"001","providerCallsign":"BBC ONE South","logicalChannelNumber":"1","matchType":"providerCallsign"},{"stationID":"17154","channel":"002","providerCallsign":"BBC TWO","logicalChannelNumber":"2","matchType":"providerCallsign"}]}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json
new file mode 100644
index 000000000..032a84e59
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json
@@ -0,0 +1 @@
+{"code":0,"serverID":"20141201.web.1","datetime":"2015-04-17T14:22:17Z","lineups":[{"lineup":"GBR-0001317-DEFAULT","name":"Freeview - Carlton - LWT (Southeast)","transport":"DVB-T","location":"London","uri":"/20141201/lineups/GBR-0001317-DEFAULT"},{"lineup":"USA-IL57303-X","name":"Comcast Waukegan/Lake Forest Area - Digital","transport":"Cable","location":"Lake Forest","uri":"/20141201/lineups/USA-IL57303-X"},{"lineup":"USA-NY67791-X","name":"Verizon Fios Queens - Digital","transport":"Cable","location":"Fresh Meadows","uri":"/20141201/lineups/USA-NY67791-X"},{"lineup":"USA-OTA-60030","name":"Local Over the Air Broadcast","transport":"Antenna","location":"60030","uri":"/20141201/lineups/USA-OTA-60030"},{"lineup":"USA-WI61859-DEFAULT","name":"DELETED LINEUP","isDeleted":true}]}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json
new file mode 100644
index 000000000..78166f09a
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json
@@ -0,0 +1 @@
+[{"programID":"SH00712240","data":[{"width":"135","height":"180","uri":"assets/p282288_b_v2_aa.jpg","size":"Sm","aspect":"3x4","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"720","height":"540","uri":"assets/p282288_b_h6_aa.jpg","size":"Lg","aspect":"4x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"960","height":"1440","uri":"assets/p282288_b_v8_aa.jpg","size":"Ms","aspect":"2x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"180","height":"135","uri":"assets/p282288_b_h5_aa.jpg","size":"Sm","aspect":"4x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"}]}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json
new file mode 100644
index 000000000..fe2a94436
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json
@@ -0,0 +1 @@
+[{"programID":"EP000000060003","titles":[{"title120":"'Allo 'Allo!"}],"eventDetails":{"subType":"Series"},"descriptions":{"description1000":[{"descriptionLanguage":"en","description":"A disguised British Intelligence officer is sent to help the airmen."}]},"originalAirDate":"1985-11-04","genres":["Sitcom"],"episodeTitle150":"The Poloceman Cometh","metadata":[{"Gracenote":{"season":2,"episode":3}}],"cast":[{"personId":"383774","nameId":"392649","name":"Gorden Kaye","role":"Actor","billingOrder":"01"},{"personId":"246840","nameId":"250387","name":"Carmen Silvera","role":"Actor","billingOrder":"02"},{"personId":"376955","nameId":"385830","name":"Rose Hill","role":"Actor","billingOrder":"03"},{"personId":"259773","nameId":"263340","name":"Vicki Michelle","role":"Actor","billingOrder":"04"},{"personId":"353113","nameId":"361987","name":"Kirsten Cooke","role":"Actor","billingOrder":"05"},{"personId":"77787","nameId":"77787","name":"Richard Marner","role":"Actor","billingOrder":"06"},{"personId":"230921","nameId":"234193","name":"Guy Siner","role":"Actor","billingOrder":"07"},{"personId":"374934","nameId":"383809","name":"Kim Hartman","role":"Actor","billingOrder":"08"},{"personId":"369151","nameId":"378026","name":"Richard Gibson","role":"Actor","billingOrder":"09"},{"personId":"343690","nameId":"352564","name":"Arthur Bostrom","role":"Actor","billingOrder":"10"},{"personId":"352557","nameId":"361431","name":"John D. Collins","role":"Actor","billingOrder":"11"},{"personId":"605275","nameId":"627734","name":"Nicholas Frankau","role":"Actor","billingOrder":"12"},{"personId":"373394","nameId":"382269","name":"Jack Haig","role":"Actor","billingOrder":"13"}],"crew":[{"personId":"354407","nameId":"363281","name":"David Croft","role":"Director","billingOrder":"01"},{"personId":"354407","nameId":"363281","name":"David Croft","role":"Writer","billingOrder":"02"},{"personId":"105145","nameId":"105145","name":"Jeremy Lloyd","role":"Writer","billingOrder":"03"}],"showType":"Series","hasImageArtwork":true,"md5":"Jo5NKxoo44xRvBCAq8QT2A"},{"programID":"EP000000510142","titles":[{"title120":"A Different World"}],"eventDetails":{"subType":"Series"},"descriptions":{"description1000":[{"descriptionLanguage":"en","description":"Whitley and Dwayne tell new students about their honeymoon in Los Angeles."}]},"originalAirDate":"1992-09-24","genres":["Sitcom"],"episodeTitle150":"Honeymoon in L.A.","metadata":[{"Gracenote":{"season":6,"episode":1}}],"cast":[{"personId":"700","nameId":"700","name":"Jasmine Guy","role":"Actor","billingOrder":"01"},{"personId":"729","nameId":"729","name":"Kadeem Hardison","role":"Actor","billingOrder":"02"},{"personId":"120","nameId":"120","name":"Darryl M. Bell","role":"Actor","billingOrder":"03"},{"personId":"1729","nameId":"1729","name":"Cree Summer","role":"Actor","billingOrder":"04"},{"personId":"217","nameId":"217","name":"Charnele Brown","role":"Actor","billingOrder":"05"},{"personId":"1811","nameId":"1811","name":"Glynn Turman","role":"Actor","billingOrder":"06"},{"personId":"1232","nameId":"1232","name":"Lou Myers","role":"Actor","billingOrder":"07"},{"personId":"1363","nameId":"1363","name":"Jada Pinkett","role":"Guest Star","billingOrder":"08"},{"personId":"222967","nameId":"225536","name":"Ajai Sanders","role":"Guest Star","billingOrder":"09"},{"personId":"181744","nameId":"183292","name":"Karen Malina White","role":"Guest Star","billingOrder":"10"},{"personId":"305017","nameId":"318897","name":"Patrick Y. Malone","role":"Guest Star","billingOrder":"11"},{"personId":"9841","nameId":"9841","name":"Bumper Robinson","role":"Guest Star","billingOrder":"12"},{"personId":"426422","nameId":"435297","name":"Sister Souljah","role":"Guest Star","billingOrder":"13"},{"personId":"25","nameId":"25","name":"Debbie Allen","role":"Guest Star","billingOrder":"14"},{"personId":"668","nameId":"668","name":"Gilbert Gottfried","role":"Guest Star","billingOrder":"15"}],"showType":"Series","hasImageArtwork":true,"md5":"P5kz0QmCeYxIA+yL0H4DWw"}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json
new file mode 100644
index 000000000..5ef1bfb1c
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json
@@ -0,0 +1 @@
+[{"stationID":"20454","date":["2015-03-13","2015-03-17"]},{"stationID":"10021","date":["2015-03-12","2015-03-13"]}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json
new file mode 100644
index 000000000..4a97e5517
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json
@@ -0,0 +1 @@
+[{"stationID":"20454","programs":[{"programID":"SH005371070000","airDateTime":"2015-03-03T00:00:00Z","duration":1800,"md5":"Sy8HEMBPcuiAx3FBukUhKQ","new":true,"audioProperties":["stereo","cc"],"videoProperties":["hdtv"]},{"programID":"EP000014577244","airDateTime":"2015-03-03T00:30:00Z","duration":1800,"md5":"25DNXVXO192JI7Y9vSW9lQ","new":true,"audioProperties":["stereo","cc"],"videoProperties":["hdtv"]}]}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json
new file mode 100644
index 000000000..e5fb64a6f
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json
@@ -0,0 +1 @@
+{"code":0,"message":"OK","serverID":"AWS-SD-web.1","datetime":"2016-08-23T13:55:25Z","token":"f3fca79989cafe7dead71beefedc812b"}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json
new file mode 100644
index 000000000..b66a4ed0c
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json
@@ -0,0 +1 @@
+{"response":"SERVICE_OFFLINE","code":3000,"serverID":"20141201.web.1","message":"Server offline for maintenance.","datetime":"2015-04-23T00:03:32Z","token":"CAFEDEADBEEFCAFEDEADBEEFCAFEDEADBEEFCAFE"}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/empty.zip b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/empty.zip
new file mode 100644
index 000000000..15628e26b
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/empty.zip
Binary files differ
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
index 70acbfc40..09c4bd100 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
@@ -6,7 +6,9 @@ using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.Archiving;
using Emby.Server.Implementations.Updates;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Updates;
using Moq;
using Moq.Protected;
@@ -40,7 +42,9 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
_fixture.Customize(new AutoMoqCustomization
{
ConfigureMembers = true
- }).Inject(http);
+ });
+ _fixture.Inject(http);
+ _fixture.Inject<IZipClient>(new ZipClient());
_installationManager = _fixture.Create<InstallationManager>();
}
@@ -78,5 +82,32 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
Assert.Single(packages);
}
+
+ [Fact]
+ public async Task InstallPackage_InvalidChecksum_ThrowsInvalidDataException()
+ {
+ var packageInfo = new InstallationInfo()
+ {
+ Name = "Test",
+ SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
+ Checksum = "InvalidChecksum"
+ };
+
+ await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async Task InstallPackage_Valid_Success()
+ {
+ var packageInfo = new InstallationInfo()
+ {
+ Name = "Test",
+ SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
+ Checksum = "11b5b2f1a9ebc4f66d6ef19018543361"
+ };
+
+ var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false);
+ Assert.Null(ex);
+ }
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
new file mode 100644
index 000000000..4421ced72
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dlna;
+using Xunit;
+using Xunit.Priority;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
+ public sealed class DlnaControllerTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private const string NonExistentProfile = "1322f35b8f2c434dad3cc07c9b97dbd1";
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+ private static string? _newDeviceProfileId;
+
+ public DlnaControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ [Priority(0)]
+ public async Task GetProfile_DoesNotExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var getResponse = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(0)]
+ public async Task DeleteProfile_DoesNotExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var getResponse = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(0)]
+ public async Task UpdateProfile_DoesNotExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var deviceProfile = new DeviceProfile()
+ {
+ Name = "ThisProfileDoesNotExist"
+ };
+
+ using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(deviceProfile, _jsonOptions));
+ content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ using var getResponse = await client.PostAsync("/Dlna/Profiles/" + NonExistentProfile, content).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(1)]
+ public async Task CreateProfile_Valid_NoContent()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var deviceProfile = new DeviceProfile()
+ {
+ Name = "ThisProfileIsNew"
+ };
+
+ using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(deviceProfile, _jsonOptions));
+ content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ using var getResponse = await client.PostAsync("/Dlna/Profiles", content).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(2)]
+ public async Task GetProfileInfos_Valid_ContainsThisProfileIsNew()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var response = await client.GetAsync("/Dlna/ProfileInfos").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 profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+
+ var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal));
+ Assert.NotNull(newProfile);
+ _newDeviceProfileId = newProfile!.Id;
+ }
+
+ [Fact]
+ [Priority(3)]
+ public async Task UpdateProfile_Valid_NoContent()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var updatedProfile = new DeviceProfile()
+ {
+ Name = "ThisProfileIsUpdated",
+ Id = _newDeviceProfileId
+ };
+
+ using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(updatedProfile, _jsonOptions));
+ content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ using var getResponse = await client.PostAsync("/Dlna/Profiles", content).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(4)]
+ public async Task DeleteProfile_Valid_NoContent()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var getResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId).ConfigureAwait(false);
+ Console.WriteLine(await getResponse.Content.ReadAsStringAsync().ConfigureAwait(false));
+ Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
index 732b4f050..2361e4aa4 100644
--- a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
@@ -21,6 +21,7 @@ namespace Jellyfin.Server.Integration.Tests
[InlineData("a=1", "a=1")] // won't be processed as it has a value
[InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed.
[InlineData("a=b&a=c", "a=b")]
+ [InlineData("a%3D1", "a=1")]
[InlineData("a%3Db%26a%3Dc", "a=b")]
public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
{
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 592b444c9..9d7b447ed 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -9,14 +9,14 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.9" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
- <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index f249be674..67ae0e080 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -10,13 +10,13 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.9" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
- <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
index 419afb2dc..d15c9d6f5 100644
--- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
+++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
@@ -12,9 +12,6 @@ namespace Jellyfin.Server.Tests
{
[Theory]
[InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded
- [InlineData("random+test", "random test")] // encoded
- [InlineData("random%20test", "random test")] // encoded
- [InlineData("++", " ")] // encoded
public static void EmptyValueTest(string query, string key)
{
var dict = new Dictionary<string, StringValues>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index e08590758..94294c8bf 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>