aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml1
-rw-r--r--.config/dotnet-tools.json12
-rw-r--r--.github/workflows/codeql-analysis.yml8
-rw-r--r--.github/workflows/commands.yml14
-rw-r--r--.github/workflows/openapi.yml12
-rw-r--r--.github/workflows/repo-bump-version.yaml82
-rw-r--r--.github/workflows/repo-stale.yaml17
-rw-r--r--CONTRIBUTORS.md3
-rw-r--r--Directory.Packages.props56
-rw-r--r--Dockerfile2
-rw-r--r--Dockerfile.arm2
-rw-r--r--Dockerfile.arm642
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs64
-rw-r--r--Emby.Dlna/DlnaManager.cs2
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj6
-rw-r--r--Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs69
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs134
-rw-r--r--Emby.Dlna/PlayTo/Device.cs131
-rw-r--r--Emby.Dlna/PlayTo/DlnaHttpClient.cs55
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs17
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs14
-rw-r--r--Emby.Naming/Common/NamingOptions.cs10
-rw-r--r--Emby.Naming/Emby.Naming.csproj6
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParser.cs2
-rw-r--r--Emby.Naming/Video/StubResolver.cs7
-rw-r--r--Emby.Photos/Emby.Photos.csproj8
-rw-r--r--Emby.Photos/PhotoProvider.cs2
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs6
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs20
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs204
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs9
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs11
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs141
-rw-r--r--Emby.Server.Implementations/Data/ConnectionPool.cs79
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs81
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs333
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs1201
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs126
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs14
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj11
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs35
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs1
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs10
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs68
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs45
-rw-r--r--Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs11
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs4
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs50
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs18
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs15
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs9
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs28
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs25
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs67
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs9
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/as.json44
-rw-r--r--Emby.Server.Implementations/Localization/Core/chr.json52
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json48
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/fo.json18
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json18
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json63
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json42
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/kn.json122
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json82
-rw-r--r--Emby.Server.Implementations/Localization/Core/ml.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/si.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json48
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zu.json11
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs37
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.csv1
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fr.csv1
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/sk.csv6
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs2
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs39
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs21
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs42
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs146
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs119
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs285
-rw-r--r--Emby.Server.Implementations/SystemManager.cs104
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs20
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs22
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs8
-rw-r--r--Jellyfin.Api/Constants/Policies.cs5
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs16
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs61
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs9
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs43
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs6
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs7
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs14
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs56
-rw-r--r--Jellyfin.Api/Controllers/TrickplayController.cs101
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs7
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs87
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs8
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs19
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs4
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj8
-rw-r--r--Jellyfin.Api/Middleware/ExceptionMiddleware.cs20
-rw-r--r--Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs3
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs2
-rw-r--r--Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs2
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs7
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs5
-rw-r--r--Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs7
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs17
-rw-r--r--Jellyfin.Data/Entities/TrickplayInfo.cs75
-rw-r--r--Jellyfin.Data/Entities/User.cs7
-rw-r--r--Jellyfin.Data/Enums/PermissionKind.cs7
-rw-r--r--Jellyfin.Data/Enums/PersonKind.cs36
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj6
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfiguration.cs2
-rw-r--r--Jellyfin.Networking/Extensions/NetworkExtensions.cs16
-rw-r--r--Jellyfin.Networking/Jellyfin.Networking.csproj6
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs244
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs1
-rw-r--r--Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs3
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj6
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs5
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs681
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs40
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs654
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs29
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs39
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs18
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs30
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs474
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs21
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs9
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs37
-rw-r--r--Jellyfin.Server/CoreAppHost.cs9
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs6
-rw-r--r--Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs3
-rw-r--r--Jellyfin.Server/Helpers/StartupHelpers.cs3
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj7
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs16
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs12
-rw-r--r--Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs55
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs51
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs40
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs14
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs25
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs11
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs15
-rw-r--r--Jellyfin.Server/Program.cs79
-rw-r--r--Jellyfin.Server/Startup.cs20
-rw-r--r--MediaBrowser.Common/Extensions/ProcessExtensions.cs60
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs21
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj10
-rw-r--r--MediaBrowser.Common/Plugins/IPluginManager.cs5
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs8
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs1
-rw-r--r--MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs1
-rw-r--r--MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs9
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs10
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs13
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs3
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs3
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs5
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs42
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/ItemImageInfo.cs8
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs2
-rw-r--r--MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs193
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs12
-rw-r--r--MediaBrowser.Controller/ISystemManager.cs34
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs2
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs147
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs32
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs4
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs2
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs6
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs1
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs2
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs6
-rw-r--r--MediaBrowser.Controller/Security/IAuthenticationManager.cs4
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs14
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs6
-rw-r--r--MediaBrowser.Controller/Trickplay/ITrickplayManager.cs76
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj6
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs505
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs9
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs80
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs27
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs291
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj6
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs18
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs54
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs6
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs4
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs510
-rw-r--r--MediaBrowser.Model/Configuration/TrickplayOptions.cs60
-rw-r--r--MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs17
-rw-r--r--MediaBrowser.Model/Configuration/UserConfiguration.cs5
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs8
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs37
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs90
-rw-r--r--MediaBrowser.Model/Drawing/ImageFormatExtensions.cs17
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs7
-rw-r--r--MediaBrowser.Model/Entities/ChapterInfo.cs7
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs9
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj7
-rw-r--r--MediaBrowser.Model/Net/IPData.cs5
-rw-r--r--MediaBrowser.Model/Net/PublishedServerUriOverride.cs42
-rw-r--r--MediaBrowser.Model/Querying/ItemFields.cs5
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs6
-rw-r--r--MediaBrowser.Model/System/CastReceiverApplication.cs17
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs9
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs8
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs6
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs3
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs36
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj8
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs10
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs128
-rw-r--r--MediaBrowser.Providers/Movies/ImdbExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs35
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs2
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs11
-rw-r--r--MediaBrowser.Providers/TV/Zap2ItExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs118
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayProvider.cs121
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj4
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs861
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs175
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs29
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs29
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs22
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs4
-rw-r--r--RSSDP/HttpRequestParser.cs5
-rw-r--r--RSSDP/HttpResponseParser.cs5
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs138
-rw-r--r--RSSDP/SsdpDevice.cs10
-rw-r--r--RSSDP/SsdpDeviceLocator.cs56
-rw-r--r--RSSDP/SsdpDevicePublisher.cs101
-rw-r--r--debian/conf/jellyfin5
-rw-r--r--debian/jellyfin.init1
-rwxr-xr-xdebian/rules4
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--fedora/jellyfin-selinux-launcher.sh3
-rw-r--r--fedora/jellyfin.env6
-rw-r--r--fedora/jellyfin.service2
-rw-r--r--fedora/jellyfin.spec5
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Program.cs9
-rwxr-xr-xfuzz/Emby.Server.Implementations.Fuzz/fuzz.sh2
-rw-r--r--fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj (renamed from fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj)4
-rw-r--r--fuzz/Jellyfin.Api.Fuzz/Program.cs (renamed from fuzz/Jellyfin.Server.Fuzz/Program.cs)4
-rw-r--r--fuzz/Jellyfin.Api.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt (renamed from fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt)0
-rwxr-xr-xfuzz/Jellyfin.Api.Fuzz/fuzz.sh11
-rwxr-xr-xfuzz/Jellyfin.Server.Fuzz/fuzz.sh11
-rw-r--r--fuzz/README.md20
-rw-r--r--global.json6
-rw-r--r--jellyfin.ruleset18
-rw-r--r--src/Directory.Build.props21
-rw-r--r--src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj15
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs163
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaHelper.cs8
-rw-r--r--src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs16
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs66
-rw-r--r--src/Jellyfin.Drawing/Jellyfin.Drawing.csproj11
-rw-r--r--src/Jellyfin.Drawing/NullImageEncoder.cs6
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj12
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs2
-rw-r--r--src/Jellyfin.Extensions/StreamExtensions.cs10
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj11
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs67
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj11
-rw-r--r--tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs (renamed from tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs)5
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs50
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs19
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs66
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs44
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs2
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs36
-rw-r--r--tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs13
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs8
-rw-r--r--tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs6
-rw-r--r--tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs56
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs10
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs20
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs8
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs114
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs34
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs60
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs172
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StackTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StubTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs56
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs36
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs1
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs10
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs39
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj3
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs4
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs4
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs4
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs61
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs8
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs24
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs10
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs10
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs18
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs111
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs34
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs23
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs44
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs19
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs10
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs12
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs28
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs26
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs20
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs23
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs33
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs57
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs8
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs3
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs2
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs4
-rw-r--r--tests/Jellyfin.Server.Tests/ParseNetworkTests.cs5
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs6
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs2
-rw-r--r--tests/jellyfin-tests.ruleset6
394 files changed, 8653 insertions, 6309 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index c28b1bf7f..c91a084e5 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -168,6 +168,7 @@ jobs:
- job: CollectArtifacts
timeoutInMinutes: 20
displayName: 'Collect Artifacts'
+ condition: succeededOrFailed()
continueOnError: true
dependsOn:
- BuildPackage
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 000000000..dbe78984a
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "7.0.13",
+ "commands": [
+ "dotnet-ef"
+ ]
+ }
+ }
+} \ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index fa32c1c0e..f43d743f0 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
+ uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
+ uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
+ uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 178959afc..75b6a73e5 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,14 +51,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index d3dfd0a6a..8c463a8fc 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -25,7 +25,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: openapi-head
retention-days: 14
@@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -59,7 +59,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: openapi-base
retention-days: 14
@@ -112,7 +112,7 @@ jobs:
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -127,7 +127,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/repo-bump-version.yaml
new file mode 100644
index 000000000..e0383afd2
--- /dev/null
+++ b/.github/workflows/repo-bump-version.yaml
@@ -0,0 +1,82 @@
+name: '🆙 Auto bump_version'
+
+on:
+ release:
+ types:
+ - published
+ workflow_dispatch:
+ inputs:
+ TAG_BRANCH:
+ required: true
+ description: release-x.y.z
+ NEXT_VERSION:
+ required: true
+ description: x.y.z
+
+jobs:
+ auto_bump_version:
+ runs-on: ubuntu-latest
+ if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }}
+ env:
+ TAG_BRANCH: ${{ github.event.release.target_commitish }}
+ steps:
+ - name: Wait for deploy checks to finish
+ uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1
+ with:
+ ref: ${{ env.TAG_BRANCH }}
+ intervalSeconds: 60
+ timeoutSeconds: 3600
+
+ - name: Setup YQ
+ uses: chrisdickinson/setup-yq@latest
+ with:
+ yq-version: v4.9.8
+
+ - name: Checkout Repository
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ ref: ${{ env.TAG_BRANCH }}
+
+ - name: Setup EnvVars
+ run: |-
+ CURRENT_VERSION=$(yq e '.version' build.yaml)
+ CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*}
+ CURRENT_PATCH=${CURRENT_VERSION##*.}
+ echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
+ echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV
+ echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV
+ echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV
+
+ - name: Run bump_version
+ run: ./bump_version ${{ env.NEXT_VERSION }}
+
+ - name: Commit Changes
+ run: |-
+ git config user.name "jellyfin-bot"
+ git config user.email "team@jellyfin.org"
+ git checkout ${{ env.TAG_BRANCH }}
+ git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
+ git push origin ${{ env.TAG_BRANCH }}
+
+ manual_bump_version:
+ runs-on: ubuntu-latest
+ if: ${{ github.event_name == 'workflow_dispatch' }}
+ env:
+ TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }}
+ NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ ref: ${{ env.TAG_BRANCH }}
+
+ - name: Run bump_version
+ run: ./bump_version ${{ env.NEXT_VERSION }}
+
+ - name: Commit Changes
+ run: |-
+ git config user.name "jellyfin-bot"
+ git config user.email "team@jellyfin.org"
+ git checkout ${{ env.TAG_BRANCH }}
+ git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
+ git push origin ${{ env.TAG_BRANCH }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index c753c1600..f9075ba03 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -8,29 +8,31 @@ on:
permissions:
issues: write
pull-requests: write
+ actions: write
jobs:
issues:
- name: Check issues
+ name: Check for stale issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
+ ascending: true
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
- operations-per-run: 75
+ operations-per-run: 500
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
- This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
+ This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
- If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
-
- This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
+ If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
+ close-issue-message: |-
+ This issue was closed due to inactivity.
prs-conflicts:
name: Check PRs with merge conflicts
@@ -40,7 +42,8 @@ jobs:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- operations-per-run: 75
+ ascending: true
+ operations-per-run: 150
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index cc9a99a2b..74f1a8965 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -57,6 +57,7 @@
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
+ - [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
@@ -88,6 +89,7 @@
- [neilsb](https://github.com/neilsb)
- [nevado](https://github.com/nevado)
- [Nickbert7](https://github.com/Nickbert7)
+ - [nicknsy](https://github.com/nicknsy)
- [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka)
- [OancaAndrei](https://github.com/OancaAndrei)
@@ -239,3 +241,4 @@
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/)
+ - [0x25CBFC4F](https://github.com/0x25CBFC4F)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index de347f3a0..5c1cd989e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,35 +2,36 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
-
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
-
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
<PackageVersion Include="AutoFixture" Version="4.18.0" />
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
- <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" />
- <PackageVersion Include="BlurHashSharp" Version="1.3.0" />
+ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.1" />
+ <PackageVersion Include="BlurHashSharp" Version="1.3.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
+ <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
+ <PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
- <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.9" />
+ <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.13" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.9" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.13" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.9" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.13" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.13" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.13" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
@@ -39,14 +40,14 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.9" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.9" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.13" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.13" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
@@ -54,28 +55,25 @@
<PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.0" />
- <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.1" />
+ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.1.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
- <PackageVersion Include="prometheus-net" Version="8.0.1" />
+ <PackageVersion Include="prometheus-net" Version="8.1.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
- <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" />
+ <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
- <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" />
+ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
- <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
- <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
- <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
- <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
- <PackageVersion Include="SkiaSharp" Version="2.88.3" />
+ <PackageVersion Include="SkiaSharp" Version="2.88.6" />
+ <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" />
+ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
- <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
- <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
- <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
+ <PackageVersion Include="Svg.Skia" Version="1.0.0.2" />
+ <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
@@ -86,8 +84,8 @@
<PackageVersion Include="TMDbLib" Version="2.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
- <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" />
+ <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageVersion Include="xunit" Version="2.4.2" />
+ <PackageVersion Include="xunit" Version="2.6.1" />
</ItemGroup>
</Project>
diff --git a/Dockerfile b/Dockerfile
index e51d285e1..9be319311 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 46a3e9b99..e8ec6398e 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 4f9d5e1fd..83137ee89 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index f668dc829..5ed982876 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress;
- private readonly string _accessToken;
- private readonly User _user;
+ private readonly string? _accessToken;
+ private readonly User? _user;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager;
@@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl
public DidlBuilder(
DeviceProfile profile,
- User user,
+ User? user,
IImageProcessor imageProcessor,
string serverAddress,
- string accessToken,
+ string? accessToken,
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
@@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl
return url + "&dlnaheaders=true";
}
- public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
+ public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo)
{
var settings = new XmlWriterSettings
{
@@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl
public void WriteItemElement(
XmlWriter writer,
BaseItem item,
- User user,
- BaseItem context,
+ User? user,
+ BaseItem? context,
StubType? contextStubType,
string deviceId,
Filter filter,
- StreamInfo streamInfo = null)
+ StreamInfo? streamInfo = null)
{
var clientId = GetClientId(item, null);
@@ -190,7 +188,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
- private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
+ private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{
if (streamInfo is null)
{
@@ -203,7 +201,7 @@ namespace Emby.Dlna.Didl
Profile = _profile,
DeviceId = deviceId,
MaxBitrate = _profile.MaxStreamingBitrate
- });
+ }) ?? throw new InvalidOperationException("No optimal video stream found");
}
var targetWidth = streamInfo.TargetWidth;
@@ -315,7 +313,7 @@ namespace Emby.Dlna.Didl
var mediaSource = streamInfo.MediaSource;
- if (mediaSource.RunTimeTicks.HasValue)
+ if (mediaSource?.RunTimeTicks.HasValue == true)
{
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
@@ -410,7 +408,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
- private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context)
+ private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context)
{
if (itemStubType.HasValue)
{
@@ -452,7 +450,7 @@ namespace Emby.Dlna.Didl
/// <param name="episode">The episode.</param>
/// <param name="context">Current context.</param>
/// <returns>Formatted name of the episode.</returns>
- private string GetEpisodeDisplayName(Episode episode, BaseItem context)
+ private string GetEpisodeDisplayName(Episode episode, BaseItem? context)
{
string[] components;
@@ -530,7 +528,7 @@ namespace Emby.Dlna.Didl
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
- private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
+ private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{
writer.WriteStartElement(string.Empty, "res", NsDidl);
@@ -544,14 +542,14 @@ namespace Emby.Dlna.Didl
MediaSources = sources.ToArray(),
Profile = _profile,
DeviceId = deviceId
- });
+ }) ?? throw new InvalidOperationException("No optimal audio stream found");
}
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
var mediaSource = streamInfo.MediaSource;
- if (mediaSource.RunTimeTicks.HasValue)
+ if (mediaSource?.RunTimeTicks is not null)
{
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
@@ -634,7 +632,7 @@ namespace Emby.Dlna.Didl
// Samsung sometimes uses 1 as root
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
- public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
+ public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null)
{
writer.WriteStartElement(string.Empty, "container", NsDidl);
@@ -678,14 +676,14 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
- private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo)
+ private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo)
{
if (!item.SupportsPositionTicksResume || item is Folder)
{
return;
}
- XmlAttribute secAttribute = null;
+ XmlAttribute? secAttribute = null;
foreach (var attribute in _profile.XmlRootAttributes)
{
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@@ -695,8 +693,8 @@ namespace Emby.Dlna.Didl
}
}
- // Not a samsung device
- if (secAttribute is null)
+ // Not a samsung device or no user data
+ if (secAttribute is null || user is null)
{
return;
}
@@ -717,7 +715,7 @@ namespace Emby.Dlna.Didl
/// <summary>
/// Adds fields used by both items and folders.
/// </summary>
- private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
+ private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
{
// Don't filter on dc:title because not all devices will include it in the filter
// MediaMonkey for example won't display content without a title
@@ -795,7 +793,7 @@ namespace Emby.Dlna.Didl
if (item.IsDisplayedAsFolder || stubType.HasValue)
{
- string classType = null;
+ string? classType = null;
if (!_profile.RequiresPlainFolders)
{
@@ -899,7 +897,7 @@ namespace Emby.Dlna.Didl
}
}
- private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
+ private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
{
AddCommonFields(item, itemStubType, context, writer, filter);
@@ -975,7 +973,7 @@ namespace Emby.Dlna.Didl
private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
{
- ImageDownloadInfo imageInfo = GetImageInfo(item);
+ ImageDownloadInfo? imageInfo = GetImageInfo(item);
if (imageInfo is null)
{
@@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
- private ImageDownloadInfo GetImageInfo(BaseItem item)
+ private ImageDownloadInfo? GetImageInfo(BaseItem item)
{
if (item.HasImage(ImageType.Primary))
{
@@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl
return null;
}
- private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
+ private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
{
if (item is null)
{
@@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
{
var imageInfo = item.GetImageInfo(type, 0);
- string tag = null;
+ string? tag = null;
try
{
@@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl
{
internal Guid ItemId { get; set; }
- internal string ImageTag { get; set; }
+ internal string? ImageTag { get; set; }
internal ImageType Type { get; set; }
@@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl
internal bool IsDirectStream { get; set; }
- internal string Format { get; set; }
+ internal required string Format { get; set; }
- internal ItemImageInfo ItemImageInfo { get; set; }
+ internal required ItemImageInfo ItemImageInfo { get; set; }
}
}
}
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 99b3e6e7e..d67cb67b5 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -228,7 +228,7 @@ namespace Emby.Dlna
try
{
return _fileSystem.GetFilePaths(path)
- .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
+ .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
.Select(i => ParseProfileFile(i, type))
.Where(i => i is not null)
.ToList()!; // We just filtered out all the nulls
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index aca239964..efbef0564 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -26,8 +26,12 @@
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
new file mode 100644
index 000000000..87ec14d95
--- /dev/null
+++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using Emby.Dlna.ConnectionManager;
+using Emby.Dlna.ContentDirectory;
+using Emby.Dlna.MediaReceiverRegistrar;
+using Emby.Dlna.Ssdp;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Rssdp.Infrastructure;
+
+namespace Emby.Dlna.Extensions;
+
+/// <summary>
+/// Extension methods for adding DLNA services.
+/// </summary>
+public static class DlnaServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds DLNA services to the provided <see cref="IServiceCollection"/>.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+ /// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param>
+ public static void AddDlnaServices(
+ this IServiceCollection services,
+ IServerApplicationHost applicationHost)
+ {
+ services.AddHttpClient(NamedClient.Dlna, c =>
+ {
+ c.DefaultRequestHeaders.UserAgent.ParseAdd(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}/{1} UPnP/1.0 {2}/{3}",
+ Environment.OSVersion.Platform,
+ Environment.OSVersion,
+ applicationHost.Name,
+ applicationHost.ApplicationVersionString));
+
+ c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
+ c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
+ })
+ .ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
+ {
+ AutomaticDecompression = DecompressionMethods.All,
+ RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
+ });
+
+ services.AddSingleton<IDlnaManager, DlnaManager>();
+ services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+ services.AddSingleton<IContentDirectory, ContentDirectoryService>();
+ services.AddSingleton<IConnectionManager, ConnectionManagerService>();
+ services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>();
+
+ services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer(
+ provider.GetRequiredService<ISocketFactory>(),
+ provider.GetRequiredService<INetworkManager>(),
+ provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>())
+ {
+ IsShared = true
+ });
+ }
+}
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 1a4bec398..aa7012487 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
using Rssdp;
using Rssdp.Infrastructure;
@@ -49,14 +47,13 @@ namespace Emby.Dlna.Main
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery;
- private readonly ISocketFactory _socketFactory;
+ private readonly ISsdpCommunicationsServer _communicationsServer;
private readonly INetworkManager _networkManager;
- private readonly object _syncLock = new object();
+ private readonly object _syncLock = new();
private readonly bool _disabled;
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
- private ISsdpCommunicationsServer _communicationsServer;
private bool _disposed;
@@ -75,10 +72,8 @@ namespace Emby.Dlna.Main
IMediaSourceManager mediaSourceManager,
IDeviceDiscovery deviceDiscovery,
IMediaEncoder mediaEncoder,
- ISocketFactory socketFactory,
- INetworkManager networkManager,
- IUserViewManager userViewManager,
- ITVSeriesManager tvSeriesManager)
+ ISsdpCommunicationsServer communicationsServer,
+ INetworkManager networkManager)
{
_config = config;
_appHost = appHost;
@@ -93,37 +88,10 @@ namespace Emby.Dlna.Main
_mediaSourceManager = mediaSourceManager;
_deviceDiscovery = deviceDiscovery;
_mediaEncoder = mediaEncoder;
- _socketFactory = socketFactory;
+ _communicationsServer = communicationsServer;
_networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
- ContentDirectory = new ContentDirectory.ContentDirectoryService(
- dlnaManager,
- userDataManager,
- imageProcessor,
- libraryManager,
- config,
- userManager,
- loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
- httpClientFactory,
- localizationManager,
- mediaSourceManager,
- userViewManager,
- mediaEncoder,
- tvSeriesManager);
-
- ConnectionManager = new ConnectionManager.ConnectionManagerService(
- dlnaManager,
- config,
- loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
- httpClientFactory);
-
- MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
- loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
- httpClientFactory,
- config);
- Current = this;
-
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
@@ -133,19 +101,6 @@ namespace Emby.Dlna.Main
}
}
- public static DlnaEntryPoint Current { get; private set; }
-
- /// <summary>
- /// Gets a value indicating whether the dlna server is enabled.
- /// </summary>
- public static bool Enabled { get; private set; }
-
- public IContentDirectory ContentDirectory { get; private set; }
-
- public IConnectionManager ConnectionManager { get; private set; }
-
- public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
-
public async Task RunAsync()
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@@ -172,9 +127,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
- Enabled = options.EnableServer;
-
- StartSsdpHandler();
+ StartDeviceDiscovery();
if (options.EnableServer)
{
@@ -195,36 +148,11 @@ namespace Emby.Dlna.Main
}
}
- private void StartSsdpHandler()
- {
- try
- {
- if (_communicationsServer is null)
- {
- var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
-
- _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
- {
- IsShared = true
- };
-
- StartDeviceDiscovery(_communicationsServer);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting ssdp handlers");
- }
- }
-
- private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
+ private void StartDeviceDiscovery()
{
try
{
- if (communicationsServer is not null)
- {
- ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
- }
+ ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
}
catch (Exception ex)
{
@@ -232,19 +160,6 @@ namespace Emby.Dlna.Main
}
}
- private void DisposeDeviceDiscovery()
- {
- try
- {
- _logger.LogInformation("Disposing DeviceDiscovery");
- ((DeviceDiscovery)_deviceDiscovery).Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error stopping device discovery");
- }
- }
-
public void StartDevicePublisher(Configuration.DlnaOptions options)
{
if (_publisher is not null)
@@ -317,7 +232,7 @@ namespace Emby.Dlna.Main
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};
- SetProperies(device, fullService);
+ SetProperties(device, fullService);
_publisher.AddDevice(device);
var embeddedDevices = new[]
@@ -338,13 +253,13 @@ namespace Emby.Dlna.Main
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};
- SetProperies(embeddedDevice, subDevice);
+ SetProperties(embeddedDevice, subDevice);
device.AddDevice(embeddedDevice);
}
}
}
- private string CreateUuid(string text)
+ private static string CreateUuid(string text)
{
if (!Guid.TryParse(text, out var guid))
{
@@ -354,15 +269,14 @@ namespace Emby.Dlna.Main
return guid.ToString("D", CultureInfo.InvariantCulture);
}
- private void SetProperies(SsdpDevice device, string fullDeviceType)
+ private static void SetProperties(SsdpDevice device, string fullDeviceType)
{
- var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
-
- var serviceParts = service.Split(':');
-
- var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
+ var serviceParts = fullDeviceType
+ .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Split(':');
- device.DeviceTypeNamespace = deviceTypeNamespace;
+ device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
device.DeviceClass = serviceParts[1];
device.DeviceType = serviceParts[2];
}
@@ -443,20 +357,6 @@ namespace Emby.Dlna.Main
DisposeDevicePublisher();
DisposePlayToManager();
- DisposeDeviceDiscovery();
-
- if (_communicationsServer is not null)
- {
- _logger.LogInformation("Disposing SsdpCommunicationsServer");
- _communicationsServer.Dispose();
- _communicationsServer = null;
- }
-
- ContentDirectory = null;
- ConnectionManager = null;
- MediaReceiverRegistrar = null;
- Current = null;
-
_disposed = true;
}
}
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 9c476119d..18fa19650 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo
private readonly ILogger _logger;
private readonly object _timerLock = new object();
- private Timer _timer;
+ private Timer? _timer;
private int _muteVol;
private int _volume;
private DateTime _lastVolumeRefresh;
@@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo
_logger = logger;
}
- public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
+ public event EventHandler<PlaybackStartEventArgs>? PlaybackStart;
- public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
+ public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress;
- public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
+ public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped;
- public event EventHandler<MediaChangedEventArgs> MediaChanged;
+ public event EventHandler<MediaChangedEventArgs>? MediaChanged;
public DeviceInfo Properties { get; set; }
@@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo
public bool IsStopped => TransportState == TransportState.STOPPED;
- public Action OnDeviceUnavailable { get; set; }
+ public Action? OnDeviceUnavailable { get; set; }
- private TransportCommands AvCommands { get; set; }
+ private TransportCommands? AvCommands { get; set; }
- private TransportCommands RendererCommands { get; set; }
+ private TransportCommands? RendererCommands { get; set; }
- public UBaseObject CurrentMediaInfo { get; private set; }
+ public UBaseObject? CurrentMediaInfo { get; private set; }
public void Start()
{
@@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo
_volumeRefreshActive = true;
var time = immediate ? 100 : 10000;
- _timer.Change(time, Timeout.Infinite);
+ _timer?.Change(time, Timeout.Infinite);
}
}
@@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo
_volumeRefreshActive = false;
- _timer.Change(Timeout.Infinite, Timeout.Infinite);
+ _timer?.Change(Timeout.Infinite, Timeout.Infinite);
}
}
@@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private DeviceService GetServiceRenderingControl()
+ private DeviceService? GetServiceRenderingControl()
{
var services = Properties.Services;
@@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo
services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
}
- private DeviceService GetAvTransportService()
+ private DeviceService? GetAvTransportService()
{
var services = Properties.Services;
@@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
- rendererCommands.BuildPost(command, service.ServiceType, value),
+ rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- var service = GetServiceRenderingControl();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
+ var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service");
// Set it early and assume it will succeed
// Remote control will perform better
@@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
- rendererCommands.BuildPost(command, service.ServiceType, value),
+ rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
@@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo
return;
}
- var service = GetAvTransportService();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
- avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
+ avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
}
- public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken)
+ public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
@@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo
{ "CurrentURIMetaData", CreateDidlMeta(metaData) }
};
- var service = GetAvTransportService();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
- var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
+ var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
@@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
* Without that information, the next track command on the device does not work.
*/
- public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
+ public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
@@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
- var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
if (command is null)
{
return;
@@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo
{ "NextURIMetaData", CreateDidlMeta(metaData) }
};
- var service = GetAvTransportService();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
- var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
+ var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
.ConfigureAwait(false);
@@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo
return Task.CompletedTask;
}
- var service = GetAvTransportService();
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
@@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo
return;
}
- var service = GetAvTransportService();
-
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
- avCommands.BuildPost(command, service.ServiceType, 1),
+ avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo
return;
}
- var service = GetAvTransportService();
-
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
- avCommands.BuildPost(command, service.ServiceType, 1),
+ avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true);
}
- private async void TimerCallback(object sender)
+ private async void TimerCallback(object? sender)
{
if (_disposed)
{
@@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
- rendererCommands.BuildPost(command, service.ServiceType),
+ rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result is null || result.Document is null)
@@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
- rendererCommands.BuildPost(command, service.ServiceType),
+ rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result is null || result.Document is null)
@@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+ private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command is null)
@@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+ private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command is null)
@@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo
return (true, null);
}
- XElement uPnpResponse = null;
+ XElement? uPnpResponse = null;
try
{
@@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo
return (true, uTrack);
}
- private XElement ParseResponse(string xml)
+ private XElement? ParseResponse(string xml)
{
// Handle different variations sent back by devices.
try
@@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
+ private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri)
{
ArgumentNullException.ThrowIfNull(container);
@@ -959,20 +927,17 @@ namespace Emby.Dlna.PlayTo
var resElement = container.Element(UPnpNamespaces.Res);
- if (resElement is not null)
- {
- var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
+ var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo);
- if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
- {
- return info.Value.Split(':');
- }
+ if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
+ {
+ return info.Value.Split(':');
}
return new string[4];
}
- private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
+ private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken)
{
if (AvCommands is not null)
{
@@ -1004,7 +969,7 @@ namespace Emby.Dlna.PlayTo
return AvCommands;
}
- private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken)
+ private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken)
{
if (RendererCommands is not null)
{
@@ -1054,7 +1019,7 @@ namespace Emby.Dlna.PlayTo
return baseUrl + url;
}
- public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
+ public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
{
var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
@@ -1171,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClientFactory, logger);
}
-#nullable enable
private static DeviceIcon CreateIcon(XElement element)
{
ArgumentNullException.ThrowIfNull(element);
@@ -1284,11 +1248,10 @@ namespace Emby.Dlna.PlayTo
if (disposing)
{
_timer?.Dispose();
+ _timer = null;
+ Properties = null!;
}
- _timer = null;
- Properties = null;
-
_disposed = true;
}
diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
index 220aa1a8d..255c51f19 100644
--- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs
+++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
@@ -55,41 +55,42 @@ namespace Emby.Dlna.PlayTo
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using MemoryStream ms = new MemoryStream();
- await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
- ms.Position = 0;
- try
+ Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
{
- return await XDocument.LoadAsync(
- ms,
- LoadOptions.None,
- cancellationToken).ConfigureAwait(false);
- }
- catch (XmlException)
- {
- // try correcting the Xml response with common errors
- ms.Position = 0;
- using StreamReader sr = new StreamReader(ms);
- var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
-
- // find and replace unescaped ampersands (&)
- xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
-
try
{
- // retry reading Xml
- using var xmlReader = new StringReader(xmlString);
return await XDocument.LoadAsync(
- xmlReader,
+ stream,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
- catch (XmlException ex)
+ catch (XmlException)
{
- _logger.LogError(ex, "Failed to parse response");
- _logger.LogDebug("Malformed response: {Content}\n", xmlString);
-
- return null;
+ // try correcting the Xml response with common errors
+ stream.Position = 0;
+ using StreamReader sr = new StreamReader(stream);
+ var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+
+ // find and replace unescaped ampersands (&)
+ xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
+
+ try
+ {
+ // retry reading Xml
+ using var xmlReader = new StringReader(xmlString);
+ return await XDocument.LoadAsync(
+ xmlReader,
+ LoadOptions.None,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (XmlException ex)
+ {
+ _logger.LogError(ex, "Failed to parse response");
+ _logger.LogDebug("Malformed response: {Content}\n", xmlString);
+
+ return null;
+ }
}
}
}
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 86db36337..df02fe631 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -42,7 +42,7 @@ namespace Emby.Dlna.PlayTo
private readonly IDeviceDiscovery _deviceDiscovery;
private readonly string _serverAddress;
- private readonly string _accessToken;
+ private readonly string? _accessToken;
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
private Device _device;
@@ -59,7 +59,7 @@ namespace Emby.Dlna.PlayTo
IUserManager userManager,
IImageProcessor imageProcessor,
string serverAddress,
- string accessToken,
+ string? accessToken,
IDeviceDiscovery deviceDiscovery,
IUserDataManager userDataManager,
ILocalizationManager localization,
@@ -683,16 +683,15 @@ namespace Emby.Dlna.PlayTo
if (disposing)
{
+ _device.PlaybackStart -= OnDevicePlaybackStart;
+ _device.PlaybackProgress -= OnDevicePlaybackProgress;
+ _device.PlaybackStopped -= OnDevicePlaybackStopped;
+ _device.MediaChanged -= OnDeviceMediaChanged;
+ _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
+ _device.OnDeviceUnavailable = null;
_device.Dispose();
}
- _device.PlaybackStart -= OnDevicePlaybackStart;
- _device.PlaybackProgress -= OnDevicePlaybackProgress;
- _device.PlaybackStopped -= OnDevicePlaybackStopped;
- _device.MediaChanged -= OnDeviceMediaChanged;
- _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
- _device.OnDeviceUnavailable = null;
-
_disposed = true;
}
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index 241dff5ae..b05e0a095 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -41,9 +39,9 @@ namespace Emby.Dlna.PlayTo
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
+ private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
+ private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private bool _disposed;
- private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
- private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
@@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
}
- private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
+ private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
{
if (_disposed)
{
@@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo
var info = e.Argument;
- if (!info.Headers.TryGetValue("USN", out string usn))
+ if (!info.Headers.TryGetValue("USN", out string? usn))
{
usn = string.Empty;
}
- if (!info.Headers.TryGetValue("NT", out string nt))
+ if (!info.Headers.TryGetValue("NT", out string? nt))
{
nt = string.Empty;
}
@@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo
var uri = info.Location;
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
- if (info.Headers.TryGetValue("USN", out string uuid))
+ if (info.Headers.TryGetValue("USN", out string? uuid))
{
uuid = GetUuid(uuid);
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 2bd089ed8..b63c8f10e 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -318,7 +318,7 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
// <!-- foo.E01., foo.e01. -->
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
- new EpisodeExpression(@"(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
+ new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
{
@@ -328,7 +328,7 @@ namespace Emby.Naming.Common
"yyyy MM dd"
}
},
- new EpisodeExpression(@"(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
+ new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
{
DateTimeFormats = new[]
{
@@ -376,7 +376,7 @@ namespace Emby.Naming.Common
IsNamed = true,
SupportsAbsoluteEpisodeNumbers = false
},
- new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$")
+ new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
{
SupportsAbsoluteEpisodeNumbers = true
},
@@ -417,7 +417,7 @@ namespace Emby.Naming.Common
},
// "1-12 episode title"
- new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
+ new EpisodeExpression("([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
@@ -712,7 +712,7 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?<chapter>[0-9]+)",
// Part if often ending of filename
- @"(?<!ch(?:apter) )(?<part>[0-9]+)$",
+ "(?<!ch(?:apter) )(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index f3973dad9..bc7548189 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -45,8 +45,12 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 953129671..4080ba10d 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
return null;
}
- var extension = Path.GetExtension(path);
+ var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index f7ba606e3..4b9df19b0 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -26,19 +26,18 @@ namespace Emby.Naming.Video
return false;
}
- var extension = Path.GetExtension(path);
+ var extension = Path.GetExtension(path.AsSpan());
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
- path = Path.GetFileNameWithoutExtension(path);
- var token = Path.GetExtension(path).TrimStart('.');
+ var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
foreach (var rule in options.StubTypes)
{
- if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
+ if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
stubType = rule.StubType;
return true;
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 0f97a0686..5a04bbe49 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -24,14 +24,18 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index f54066c57..27329a7f2 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -61,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
- if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
+ if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 6edfad575..39524be1d 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
public abstract class BaseApplicationPaths : IApplicationPaths
{
- private string _dataPath;
-
/// <summary>
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
/// </summary>
@@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
- _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
+ DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <summary>
@@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
- public string DataPath => _dataPath;
+ public string DataPath { get; }
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index a4deeddb7..a2f38c8c2 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
public abstract class BaseConfigurationManager : IConfigurationManager
{
- private readonly IFileSystem _fileSystem;
-
- private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
-
- /// <summary>
- /// The _configuration sync lock.
- /// </summary>
- private readonly object _configurationSyncLock = new object();
+ private readonly ConcurrentDictionary<string, object> _configurations = new();
+ private readonly object _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
/// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
- /// <param name="fileSystem">The file system.</param>
- protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+ protected BaseConfigurationManager(
+ IApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IXmlSerializer xmlSerializer)
{
CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
- _fileSystem = fileSystem;
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
UpdateCachePath();
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
{
var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty);
- _fileSystem.DeleteFile(file);
+ File.Delete(file);
}
private string GetConfigurationFile(string key)
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index dd90a8950..c9bf7f085 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -12,11 +12,8 @@ using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
-using System.Threading;
using System.Threading.Tasks;
-using Emby.Dlna;
using Emby.Dlna.Main;
-using Emby.Dlna.Ssdp;
using Emby.Naming.Common;
using Emby.Photos;
using Emby.Server.Implementations.Channels;
@@ -59,7 +56,6 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -83,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
@@ -112,7 +107,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Class CompositionRoot.
/// </summary>
- public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
+ public abstract class ApplicationHost : IServerApplicationHost, IDisposable
{
/// <summary>
/// The disposable parts.
@@ -120,14 +115,12 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly DeviceId _deviceId;
- private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
- private ISessionManager _sessionManager;
/// <summary>
/// Gets or sets all concrete types.
@@ -135,7 +128,7 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
- private bool _disposed = false;
+ private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -154,10 +147,8 @@ namespace Emby.Server.Implementations
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
- _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
- _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
@@ -165,13 +156,15 @@ namespace Emby.Server.Implementations
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
- ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+ ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
+
+ _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
}
/// <summary>
@@ -186,23 +179,16 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; }
- public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
- && !_startupOptions.IsService
- && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
-
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
public INetworkManager NetManager { get; private set; }
- /// <summary>
- /// Gets a value indicating whether this instance has changes that require the entire application to restart.
- /// </summary>
- /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
+ /// <inheritdoc />
public bool HasPendingRestart { get; private set; }
/// <inheritdoc />
- public bool IsShuttingDown { get; private set; }
+ public bool ShouldRestart { get; set; }
/// <summary>
/// Gets the logger.
@@ -406,11 +392,9 @@ namespace Emby.Server.Implementations
/// <summary>
/// Runs the startup tasks.
/// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
- public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
+ public async Task RunStartupTasksAsync()
{
- cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@@ -424,8 +408,6 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>();
- cancellationToken.ThrowIfCancellationRequested();
-
var stopWatch = new Stopwatch();
stopWatch.Start();
@@ -435,8 +417,6 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
- cancellationToken.ThrowIfCancellationRequested();
-
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -466,7 +446,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
- NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
+ NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
@@ -509,7 +489,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- serviceCollection.AddSingleton(_fileSystemManager);
+ serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
+ serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
+
+ serviceCollection.AddScoped<ISystemManager, SystemManager>();
+
serviceCollection.AddSingleton<TmdbClientManager>();
serviceCollection.AddSingleton(NetManager);
@@ -575,8 +559,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
- serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
-
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@@ -588,8 +570,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
-
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
@@ -633,8 +613,6 @@ namespace Emby.Server.Implementations
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
- _sessionManager = Resolve<ISessionManager>();
-
SetStaticProperties();
FindParts();
@@ -685,7 +663,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
- BaseItem.FileSystem = _fileSystemManager;
+ BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
Video.LiveTvManager = Resolve<ILiveTvManager>();
@@ -856,38 +834,6 @@ namespace Emby.Server.Implementations
}
/// <summary>
- /// Restarts this instance.
- /// </summary>
- public void Restart()
- {
- if (IsShuttingDown)
- {
- return;
- }
-
- IsShuttingDown = true;
- _pluginManager.UnloadAssemblies();
-
- Task.Run(async () =>
- {
- try
- {
- await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error sending server restart notification");
- }
-
- Logger.LogInformation("Calling RestartInternal");
-
- RestartInternal();
- });
- }
-
- protected abstract void RestartInternal();
-
- /// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
@@ -942,49 +888,6 @@ namespace Emby.Server.Implementations
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
- /// <summary>
- /// Gets the system status.
- /// </summary>
- /// <param name="request">Where this request originated.</param>
- /// <returns>SystemInfo.</returns>
- public SystemInfo GetSystemInfo(HttpRequest request)
- {
- return new SystemInfo
- {
- HasPendingRestart = HasPendingRestart,
- IsShuttingDown = IsShuttingDown,
- Version = ApplicationVersionString,
- WebSocketPortNumber = HttpPort,
- CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
- Id = SystemId,
- ProgramDataPath = ApplicationPaths.ProgramDataPath,
- WebPath = ApplicationPaths.WebPath,
- LogPath = ApplicationPaths.LogDirectoryPath,
- ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
- InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
- CachePath = ApplicationPaths.CachePath,
- CanLaunchWebBrowser = CanLaunchWebBrowser,
- TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
- ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(request),
- SupportsLibraryMonitor = true,
- PackageName = _startupOptions.PackageName
- };
- }
-
- public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
- {
- return new PublicSystemInfo
- {
- Version = ApplicationVersionString,
- ProductName = ApplicationProductName,
- Id = SystemId,
- ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(request),
- StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
- };
- }
-
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr)
{
@@ -1002,11 +905,11 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public string GetSmartApiUrl(HttpRequest request)
{
- // Return the host in the HTTP request as the API url
+ // Return the host in the HTTP request as the API URL if not configured otherwise
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
{
int? requestPort = request.Host.Port;
- if (requestPort == null
+ if (requestPort is null
|| (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|| (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
{
@@ -1037,7 +940,7 @@ namespace Emby.Server.Implementations
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
{
// With an empty source, the port will be null
- var smart = NetManager.GetBindAddress(ipAddress, out _, true);
+ var smart = NetManager.GetBindAddress(ipAddress, out _, false);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart, scheme, port);
@@ -1065,30 +968,6 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/');
}
- /// <inheritdoc />
- public async Task Shutdown()
- {
- if (IsShuttingDown)
- {
- return;
- }
-
- IsShuttingDown = true;
-
- try
- {
- await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error sending server shutdown notification");
- }
-
- ShutdownInternal();
- }
-
- protected abstract void ShutdownInternal();
-
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
@@ -1152,52 +1031,5 @@ namespace Emby.Server.Implementations
_disposed = true;
}
-
- public async ValueTask DisposeAsync()
- {
- await DisposeAsyncCore().ConfigureAwait(false);
- Dispose(false);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
- /// </summary>
- /// <returns>A ValueTask.</returns>
- protected virtual async ValueTask DisposeAsyncCore()
- {
- var type = GetType();
-
- Logger.LogInformation("Disposing {Type}", type.Name);
-
- foreach (var (part, _) in _disposableParts)
- {
- var partType = part.GetType();
- if (partType == type)
- {
- continue;
- }
-
- Logger.LogInformation("Disposing {Type}", partType.Name);
-
- try
- {
- part.Dispose();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error disposing {Type}", partType.Name);
- }
- }
-
- if (_sessionManager != null)
- {
- // used for closing websockets
- foreach (var session in _sessionManager.Sessions)
- {
- await session.DisposeAsync().ConfigureAwait(false);
- }
- }
- }
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 961e225e9..8279acb05 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
- await using FileStream createStream = File.Create(path);
- await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
+ FileStream createStream = File.Create(path);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
+ }
}
/// <inheritdoc />
@@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
if (info.People is not null && info.People.Count > 0)
{
- _libraryManager.UpdatePeople(item, info.People);
+ await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
}
}
else if (forceUpdate)
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index 6b8b1a620..0ee43ce0a 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
/// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
- /// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param>
+ /// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
- /// <param name="fileSystem">The file system.</param>
- public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
- : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
+ public ServerConfigurationManager(
+ IApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IXmlSerializer xmlSerializer)
+ : base(applicationPaths, loggerFactory, xmlSerializer)
{
UpdateMetadataPath();
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index d05534ee7..bf079d90c 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using Jellyfin.Extensions;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
@@ -46,24 +46,6 @@ namespace Emby.Server.Implementations.Data
protected ILogger<BaseSqliteRepository> Logger { get; }
/// <summary>
- /// Gets the default connection flags.
- /// </summary>
- /// <value>The default connection flags.</value>
- protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
-
- /// <summary>
- /// Gets the transaction mode.
- /// </summary>
- /// <value>The transaction mode.</value>>
- protected TransactionMode TransactionMode => TransactionMode.Deferred;
-
- /// <summary>
- /// Gets the transaction mode for read-only operations.
- /// </summary>
- /// <value>The transaction mode.</value>
- protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
-
- /// <summary>
/// Gets the cache size.
/// </summary>
/// <value>The cache size or null.</value>
@@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data
/// <see cref="SynchronousMode"/>
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
- /// <summary>
- /// Gets or sets the write lock.
- /// </summary>
- /// <value>The write lock.</value>
- protected ConnectionPool WriteConnections { get; set; }
-
- /// <summary>
- /// Gets or sets the write connection.
- /// </summary>
- /// <value>The write connection.</value>
- protected ConnectionPool ReadConnections { get; set; }
-
public virtual void Initialize()
{
- WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
- ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
-
// Configuration and pragmas can affect VACUUM so it needs to be last.
using (var connection = GetConnection())
{
@@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data
}
}
- protected ManagedConnection GetConnection(bool readOnly = false)
- => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
-
- protected SQLiteDatabaseConnection CreateWriteConnection()
- {
- var writeConnection = SQLite3.Open(
- DbFilePath,
- DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
- null);
-
- if (CacheSize.HasValue)
- {
- writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(LockingMode))
- {
- writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
- }
-
- if (!string.IsNullOrWhiteSpace(JournalMode))
- {
- writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
- }
-
- if (JournalSizeLimit.HasValue)
- {
- writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
- }
-
- if (Synchronous.HasValue)
- {
- writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
- }
-
- if (PageSize.HasValue)
- {
- writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
- }
-
- writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
- return writeConnection;
- }
-
- protected SQLiteDatabaseConnection CreateReadConnection()
+ protected SqliteConnection GetConnection()
{
- var connection = SQLite3.Open(
- DbFilePath,
- DefaultConnectionFlags | ConnectionFlags.ReadOnly,
- null);
+ var connection = new SqliteConnection($"Filename={DbFilePath}");
+ connection.Open();
if (CacheSize.HasValue)
{
@@ -208,39 +128,38 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
+ if (PageSize.HasValue)
+ {
+ connection.Execute("PRAGMA page_size=" + PageSize.Value);
+ }
+
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection;
}
- public IStatement PrepareStatement(ManagedConnection connection, string sql)
- => connection.PrepareStatement(sql);
-
- public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
- => connection.PrepareStatement(sql);
+ public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
+ {
+ var command = connection.CreateCommand();
+ command.CommandText = sql;
+ return command;
+ }
- protected bool TableExists(ManagedConnection connection, string name)
+ protected bool TableExists(SqliteConnection connection, string name)
{
- return connection.RunInTransaction(
- db =>
+ using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
+ foreach (var row in statement.ExecuteQuery())
+ {
+ if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
{
- using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
- {
- foreach (var row in statement.ExecuteQuery())
- {
- if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- }
-
- return false;
- },
- ReadTransactionMode);
+ return true;
+ }
+ }
+
+ return false;
}
- protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
+ protected List<string> GetColumnNames(SqliteConnection connection, string table)
{
var columnNames = new List<string>();
@@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data
return columnNames;
}
- protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
+ protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
@@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data
return;
}
- if (dispose)
- {
- WriteConnections.Dispose();
- ReadConnections.Dispose();
- }
-
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs
deleted file mode 100644
index 5ea7e934f..000000000
--- a/Emby.Server.Implementations/Data/ConnectionPool.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data;
-
-/// <summary>
-/// A pool of SQLite Database connections.
-/// </summary>
-public sealed class ConnectionPool : IDisposable
-{
- private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new();
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ConnectionPool" /> class.
- /// </summary>
- /// <param name="count">The number of database connection to create.</param>
- /// <param name="factory">Factory function to create the database connections.</param>
- public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory)
- {
- for (int i = 0; i < count; i++)
- {
- _connections.Add(factory.Invoke());
- }
- }
-
- /// <summary>
- /// Gets a database connection from the pool if one is available, otherwise blocks.
- /// </summary>
- /// <returns>A database connection.</returns>
- public ManagedConnection GetConnection()
- {
- if (_disposed)
- {
- ThrowObjectDisposedException();
- }
-
- return new ManagedConnection(_connections.Take(), this);
-
- static void ThrowObjectDisposedException()
- {
- throw new ObjectDisposedException(nameof(ConnectionPool));
- }
- }
-
- /// <summary>
- /// Return a database connection to the pool.
- /// </summary>
- /// <param name="connection">The database connection to return.</param>
- public void Return(SQLiteDatabaseConnection connection)
- {
- if (_disposed)
- {
- connection.Dispose();
- return;
- }
-
- _connections.Add(connection);
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- foreach (var connection in _connections)
- {
- connection.Dispose();
- }
-
- _connections.Dispose();
-
- _disposed = true;
- }
-}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
deleted file mode 100644
index e84ed8f91..000000000
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
- public sealed class ManagedConnection : IDisposable
- {
- private readonly ConnectionPool _pool;
-
- private SQLiteDatabaseConnection _db;
-
- private bool _disposed = false;
-
- public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
- {
- _db = db;
- _pool = pool;
- }
-
- public IStatement PrepareStatement(string sql)
- {
- return _db.PrepareStatement(sql);
- }
-
- public IEnumerable<IStatement> PrepareAll(string sql)
- {
- return _db.PrepareAll(sql);
- }
-
- public void ExecuteAll(string sql)
- {
- _db.ExecuteAll(sql);
- }
-
- public void Execute(string sql, params object[] values)
- {
- _db.Execute(sql, values);
- }
-
- public void RunQueries(string[] sql)
- {
- _db.RunQueries(sql);
- }
-
- public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode)
- {
- _db.RunInTransaction(action, mode);
- }
-
- public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode)
- {
- return _db.RunInTransaction(action, mode);
- }
-
- public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
- {
- return _db.Query(sql);
- }
-
- public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
- {
- return _db.Query(sql, values);
- }
-
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _pool.Return(_db);
-
- _db = null!; // Don't dispose it
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 4055b0ba1..01b5fdaee 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -1,11 +1,10 @@
-#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.Diagnostics;
+using System.Data;
using System.Globalization;
-using SQLitePCL.pretty;
+using Microsoft.Data.Sqlite;
namespace Emby.Server.Implementations.Data
{
@@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data
"yy-MM-dd"
};
- public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries)
+ public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText)
{
- ArgumentNullException.ThrowIfNull(queries);
+ if (sqliteConnection.State != ConnectionState.Open)
+ {
+ sqliteConnection.Open();
+ }
- connection.RunInTransaction(conn =>
+ using var command = sqliteConnection.CreateCommand();
+ command.CommandText = commandText;
+ using (var reader = command.ExecuteReader())
{
- conn.ExecuteAll(string.Join(';', queries));
- });
+ while (reader.Read())
+ {
+ yield return reader;
+ }
+ }
}
- public static Guid ReadGuidFromBlob(this ResultSetValue result)
+ public static void Execute(this SqliteConnection sqliteConnection, string commandText)
{
- return new Guid(result.ToBlob());
+ using var command = sqliteConnection.CreateCommand();
+ command.CommandText = commandText;
+ command.ExecuteNonQuery();
}
public static string ToDateTimeParamValue(this DateTime dateValue)
@@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
- public static DateTime ReadDateTime(this ResultSetValue result)
- {
- var dateText = result.ToString();
-
- return DateTime.ParseExact(
- dateText,
- _datetimeFormats,
- DateTimeFormatInfo.InvariantInfo,
- DateTimeStyles.AdjustToUniversal);
- }
-
- public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
+ public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- var dateText = item.ToString();
+ var dateText = reader.GetString(index);
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
@@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data
return false;
}
- public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
+ public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ReadGuidFromBlob();
+ result = reader.GetGuid(index);
return true;
}
- public static bool IsDbNull(this ResultSetValue result)
+ public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
{
- return result.SQLiteType == SQLiteType.Null;
- }
-
- public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
- {
- return result[index].ToString();
- }
+ result = string.Empty;
- public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
- {
- result = null;
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
return false;
}
- result = item.ToString();
+ result = reader.GetString(index);
return true;
}
- public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
- {
- return result[index].ToBool();
- }
-
- public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
+ public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToBool();
+ result = reader.GetBoolean(index);
return true;
}
- public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
+ public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToInt();
+ result = reader.GetInt32(index);
return true;
}
- public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
+ public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result)
{
- return result[index].ToInt64();
- }
-
- public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
- {
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToInt64();
+ result = reader.GetInt64(index);
return true;
}
- public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
+ public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToFloat();
+ result = reader.GetFloat(index);
return true;
}
- public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
+ public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToDouble();
+ result = reader.GetDouble(index);
return true;
}
- public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
+ public static void TryBind(this SqliteCommand statement, string name, Guid value)
{
- return result[index].ReadGuidFromBlob();
+ statement.TryBind(name, value, true);
}
- [Conditional("DEBUG")]
- private static void CheckName(string name)
+ public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false)
{
- throw new ArgumentException("Invalid param name: " + name, nameof(name));
- }
-
- public static void TryBind(this IStatement statement, string name, double value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
+ var preparedValue = value ?? DBNull.Value;
+ if (statement.Parameters.Contains(name))
{
- bindParam.Bind(value);
+ statement.Parameters[name].Value = preparedValue;
}
else
{
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, string value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- if (value is null)
+ // Blobs aren't always detected automatically
+ if (isBlob)
{
- bindParam.BindNull();
+ statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value });
}
else
{
- bindParam.Bind(value);
+ statement.Parameters.AddWithValue(name, preparedValue);
}
}
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, bool value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, float value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, int value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, Guid value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- Span<byte> byteValue = stackalloc byte[16];
- value.TryWriteBytes(byteValue);
- bindParam.Bind(byteValue);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, DateTime value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value.ToDateTimeParamValue());
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, long value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBindNull(this IStatement statement, string name)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.BindNull();
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, DateTime? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, Guid? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, double? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
}
- public static void TryBind(this IStatement statement, string name, int? value)
+ public static void TryBindNull(this SqliteCommand statement, string name)
{
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
+ statement.TryBind(name, DBNull.Value);
}
- public static void TryBind(this IStatement statement, string name, float? value)
+ public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command)
{
- if (value.HasValue)
+ using (var reader = command.ExecuteReader())
{
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
+ while (reader.Read())
+ {
+ yield return reader;
+ }
}
}
- public static void TryBind(this IStatement statement, string name, bool? value)
+ public static int SelectScalarInt(this SqliteCommand command)
{
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
+ var result = command.ExecuteScalar();
+ // Can't be null since the method is used to retrieve Count
+ return Convert.ToInt32(result!, CultureInfo.InvariantCulture);
}
- public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
+ public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql)
{
- while (statement.MoveNext())
- {
- yield return statement.Current;
- }
+ var command = sqliteConnection.CreateCommand();
+ command.CommandText = sql;
+ return command;
}
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 73ec856fc..e519364c2 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
-using System.Buffers.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -26,7 +25,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
@@ -35,9 +33,9 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
@@ -437,128 +435,126 @@ namespace Emby.Server.Implementations.Data
};
using (var connection = GetConnection())
- {
- connection.RunQueries(queries);
-
- connection.RunInTransaction(
- db =>
- {
- var existingColumnNames = GetColumnNames(db, "AncestorIds");
- AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
-
- existingColumnNames = GetColumnNames(db, "TypedBaseItems");
-
- AddColumn(db, "TypedBaseItems", "Path", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ChannelId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "CustomRating", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Name", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "MediaType", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Overview", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ParentId", "GUID", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Genres", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SortName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "LockedFields", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Studios", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Audio", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Tags", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "UnratedType", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "TopParentId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "CriticRating", "Float", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "CleanName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeasonName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Images", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExtraType", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Artists", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Width", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Height", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
-
- existingColumnNames = GetColumnNames(db, "ItemValues");
- AddColumn(db, "ItemValues", "CleanValue", "Text", existingColumnNames);
-
- existingColumnNames = GetColumnNames(db, ChaptersTableName);
- AddColumn(db, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames);
-
- existingColumnNames = GetColumnNames(db, "MediaStreams");
- AddColumn(db, "MediaStreams", "IsAvc", "BIT", existingColumnNames);
- AddColumn(db, "MediaStreams", "TimeBase", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "Title", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "Comment", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "CodecTag", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "BitDepth", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "RefFrames", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames);
-
- AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
-
- AddColumn(db, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvProfile", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvLevel", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
-
- AddColumn(db, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
- },
- TransactionMode);
-
- connection.RunQueries(postQueries);
+ using (var transaction = connection.BeginTransaction())
+ {
+ connection.Execute(string.Join(';', queries));
+
+ var existingColumnNames = GetColumnNames(connection, "AncestorIds");
+ AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
+
+ existingColumnNames = GetColumnNames(connection, "TypedBaseItems");
+
+ AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
+
+ existingColumnNames = GetColumnNames(connection, "ItemValues");
+ AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames);
+
+ existingColumnNames = GetColumnNames(connection, ChaptersTableName);
+ AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames);
+
+ existingColumnNames = GetColumnNames(connection, "MediaStreams");
+ AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames);
+
+ AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
+
+ AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
+
+ AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
+
+ connection.Execute(string.Join(';', postQueries));
+
+ transaction.Commit();
}
}
@@ -568,21 +564,15 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
- {
- saveImagesStatement.TryBind("@Id", item.Id);
- saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
+ var images = SerializeImages(item.ImageInfos);
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id");
+ saveImagesStatement.TryBind("@Id", item.Id);
+ saveImagesStatement.TryBind("@Images", images);
- saveImagesStatement.MoveNext();
- }
- },
- TransactionMode);
- }
+ saveImagesStatement.ExecuteNonQuery();
+ transaction.Commit();
}
/// <summary>
@@ -618,18 +608,13 @@ namespace Emby.Server.Implementations.Data
tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
}
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- SaveItemsInTransaction(db, tuples);
- },
- TransactionMode);
- }
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ SaveItemsInTransaction(connection, tuples);
+ transaction.Commit();
}
- private void SaveItemsInTransaction(IDatabaseConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
+ private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
{
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
@@ -639,7 +624,8 @@ namespace Emby.Server.Implementations.Data
{
if (requiresReset)
{
- saveItemStatement.Reset();
+ saveItemStatement.Parameters.Clear();
+ deleteAncestorsStatement.Parameters.Clear();
}
var item = tuple.Item;
@@ -677,7 +663,7 @@ namespace Emby.Server.Implementations.Data
return _appHost.ExpandVirtualPath(path);
}
- private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, IStatement saveItemStatement)
+ private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement)
{
Type type = item.GetType();
@@ -686,7 +672,7 @@ namespace Emby.Server.Implementations.Data
if (TypeRequiresDeserialization(type))
{
- saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions));
+ saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true);
}
else
{
@@ -1033,7 +1019,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@OwnerId", ownerId);
}
- saveItemStatement.MoveNext();
+ saveItemStatement.ExecuteNonQuery();
}
internal static string SerializeProviderIds(Dictionary<string, string> providerIds)
@@ -1287,7 +1273,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@@ -1305,96 +1291,35 @@ namespace Emby.Server.Implementations.Data
{
if (_config.Configuration.SkipDeserializationForBasicTypes)
{
- if (type == typeof(Channel))
+ if (type == typeof(Channel)
+ || type == typeof(UserRootFolder))
{
return false;
}
-
- if (type == typeof(UserRootFolder))
- {
- return false;
- }
- }
-
- if (type == typeof(Season))
- {
- return false;
- }
-
- if (type == typeof(MusicArtist))
- {
- return false;
- }
-
- if (type == typeof(Person))
- {
- return false;
}
- if (type == typeof(MusicGenre))
- {
- return false;
- }
-
- if (type == typeof(Genre))
- {
- return false;
- }
-
- if (type == typeof(Studio))
- {
- return false;
- }
-
- if (type == typeof(PlaylistsFolder))
- {
- return false;
- }
-
- if (type == typeof(PhotoAlbum))
- {
- return false;
- }
-
- if (type == typeof(Year))
- {
- return false;
- }
-
- if (type == typeof(Book))
- {
- return false;
- }
-
- if (type == typeof(LiveTvProgram))
- {
- return false;
- }
-
- if (type == typeof(AudioBook))
- {
- return false;
- }
-
- if (type == typeof(Audio))
- {
- return false;
- }
-
- if (type == typeof(MusicAlbum))
- {
- return false;
- }
-
- return true;
+ return type != typeof(Season)
+ && type != typeof(MusicArtist)
+ && type != typeof(Person)
+ && type != typeof(MusicGenre)
+ && type != typeof(Genre)
+ && type != typeof(Studio)
+ && type != typeof(PlaylistsFolder)
+ && type != typeof(PhotoAlbum)
+ && type != typeof(Year)
+ && type != typeof(Book)
+ && type != typeof(LiveTvProgram)
+ && type != typeof(AudioBook)
+ && type != typeof(Audio)
+ && type != typeof(MusicAlbum);
}
- private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query)
+ private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
{
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query));
}
- private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
+ private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
{
var typeString = reader.GetString(0);
@@ -1411,7 +1336,7 @@ namespace Emby.Server.Implementations.Data
{
try
{
- item = JsonSerializer.Deserialize(reader[1].ToBlob(), type, _jsonOptions) as BaseItem;
+ item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem;
}
catch (JsonException ex)
{
@@ -1452,17 +1377,9 @@ namespace Emby.Server.Implementations.Data
item.EndDate = endDate;
}
- var channelId = reader[index];
- if (!channelId.IsDbNull())
+ if (reader.TryGetGuid(index, out var guid))
{
- if (!Utf8Parser.TryParse(channelId.ToBlob(), out Guid value, out _, standardFormat: 'N'))
- {
- var str = reader.GetString(index);
- Logger.LogWarning("{ChannelId} isn't in the expected format", str);
- value = new Guid(str);
- }
-
- item.ChannelId = value;
+ item.ChannelId = guid;
}
index++;
@@ -1978,7 +1895,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
var chapters = new List<ChapterInfo>();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
{
statement.TryBind("@ItemId", item.Id);
@@ -1997,7 +1914,7 @@ namespace Emby.Server.Implementations.Data
{
CheckDisposed();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
{
statement.TryBind("@ItemId", item.Id);
@@ -2018,7 +1935,7 @@ namespace Emby.Server.Implementations.Data
/// <param name="reader">The reader.</param>
/// <param name="item">The item.</param>
/// <returns>ChapterInfo.</returns>
- private ChapterInfo GetChapter(IReadOnlyList<ResultSetValue> reader, BaseItem item)
+ private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item)
{
var chapter = new ChapterInfo
{
@@ -2033,18 +1950,7 @@ namespace Emby.Server.Implementations.Data
if (reader.TryGetString(2, out var imagePath))
{
chapter.ImagePath = imagePath;
-
- if (!string.IsNullOrEmpty(chapter.ImagePath))
- {
- try
- {
- chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Failed to create image cache tag.");
- }
- }
+ chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
}
if (reader.TryReadDateTime(3, out var imageDateModified))
@@ -2071,23 +1977,18 @@ namespace Emby.Server.Implementations.Data
ArgumentNullException.ThrowIfNull(chapters);
- var idBlob = id.ToByteArray();
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- // First delete chapters
- db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ // First delete chapters
+ using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId");
+ command.TryBind("@ItemId", id);
+ command.ExecuteNonQuery();
- InsertChapters(idBlob, chapters, db);
- },
- TransactionMode);
- }
+ InsertChapters(id, chapters, connection);
+ transaction.Commit();
}
- private void InsertChapters(byte[] idBlob, IReadOnlyList<ChapterInfo> chapters, IDatabaseConnection db)
+ private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db)
{
var startIndex = 0;
var limit = 100;
@@ -2105,7 +2006,7 @@ namespace Emby.Server.Implementations.Data
insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
}
- insertText.Length -= 1; // Remove last ,
+ insertText.Length -= 1; // Remove trailing comma
using (var statement = PrepareStatement(db, insertText.ToString()))
{
@@ -2126,8 +2027,7 @@ namespace Emby.Server.Implementations.Data
chapterIndex++;
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
startIndex += limit;
@@ -2463,7 +2363,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private void BindSearchParams(InternalItemsQuery query, IStatement statement)
+ private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement)
{
var searchTerm = query.SearchTerm;
@@ -2475,7 +2375,7 @@ namespace Emby.Server.Implementations.Data
searchTerm = FixUnicodeChars(searchTerm);
searchTerm = GetCleanValue(searchTerm);
- var commandText = statement.SQL;
+ var commandText = statement.CommandText;
if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase))
{
statement.TryBind("@SearchTermStartsWith", searchTerm + "%");
@@ -2492,7 +2392,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private void BindSimilarParams(InternalItemsQuery query, IStatement statement)
+ private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement)
{
var item = query.SimilarTo;
@@ -2501,7 +2401,7 @@ namespace Emby.Server.Implementations.Data
return;
}
- var commandText = statement.SQL;
+ var commandText = statement.CommandText;
if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase))
{
@@ -2584,7 +2484,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2598,7 +2498,7 @@ namespace Emby.Server.Implementations.Data
// Running this again will bind the params
GetWhereClauses(query, statement);
- return statement.ExecuteQuery().SelectScalarInt().First();
+ return statement.SelectScalarInt();
}
}
@@ -2652,7 +2552,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2860,69 +2760,65 @@ namespace Emby.Server.Implementations.Data
var list = new List<BaseItem>();
var result = new QueryResult<BaseItem>();
- using (var connection = GetConnection(true))
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ if (!isReturningZeroItems)
{
- connection.RunInTransaction(
- db =>
+ using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
+ using (var statement = PrepareStatement(connection, itemQuery))
+ {
+ if (EnableJoinUserData(query))
+ {
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
+
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+
+ // Running this again will bind the params
+ GetWhereClauses(query, statement);
+
+ var hasEpisodeAttributes = HasEpisodeAttributes(query);
+ var hasServiceName = HasServiceName(query);
+ var hasProgramAttributes = HasProgramAttributes(query);
+ var hasStartDate = HasStartDate(query);
+ var hasTrailerTypes = HasTrailerTypes(query);
+ var hasArtistFields = HasArtistFields(query);
+ var hasSeriesFields = HasSeriesFields(query);
+
+ foreach (var row in statement.ExecuteQuery())
{
- if (!isReturningZeroItems)
+ var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+ if (item is not null)
{
- using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
- using (var statement = PrepareStatement(db, itemQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
- if (item is not null)
- {
- list.Add(item);
- }
- }
- }
+ list.Add(item);
}
+ }
+ }
+ }
- if (query.EnableTotalRecordCount)
- {
- using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
- using (var statement = PrepareStatement(db, totalRecordCountQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
+ if (query.EnableTotalRecordCount)
+ {
+ using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
+ using (var statement = PrepareStatement(connection, totalRecordCountQuery))
+ {
+ if (EnableJoinUserData(query))
+ {
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
- // Running this again will bind the params
- GetWhereClauses(query, statement);
+ // Running this again will bind the params
+ GetWhereClauses(query, statement);
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
- }
- }
- },
- ReadTransactionMode);
+ result.TotalRecordCount = statement.SelectScalarInt();
+ }
}
+ transaction.Commit();
+
result.StartIndex = query.StartIndex ?? 0;
result.Items = list;
return result;
@@ -3172,7 +3068,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var list = new List<Guid>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -3188,7 +3084,7 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery())
{
- list.Add(row[0].ReadGuidFromBlob());
+ list.Add(row.GetGuid(0));
}
}
@@ -3224,7 +3120,7 @@ namespace Emby.Server.Implementations.Data
}
#nullable enable
- private List<string> GetWhereClauses(InternalItemsQuery query, IStatement? statement)
+ private List<string> GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement)
{
if (query.IsResumable ?? false)
{
@@ -3604,7 +3500,6 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
}
- // Remove last " OR "
clauseBuilder.Length -= Or.Length;
clauseBuilder.Append(')');
@@ -3645,14 +3540,9 @@ namespace Emby.Server.Implementations.Data
.Append(paramName)
.Append("))) OR ");
- if (statement is not null)
- {
- query.PersonIds[i].TryWriteBytes(idBytes);
- statement.TryBind(paramName, idBytes);
- }
+ statement?.TryBind(paramName, query.PersonIds[i]);
}
- // Remove last " OR "
clauseBuilder.Length -= Or.Length;
clauseBuilder.Append(')');
@@ -3819,215 +3709,219 @@ namespace Emby.Server.Implementations.Data
if (query.ArtistIds.Length > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var artistId in query.ArtistIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.ArtistIds.Length; i++)
{
- var paramName = "@ArtistIds" + index;
- clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
- statement?.TryBind(paramName, artistId);
- index++;
+ clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
+ .Append(i)
+ .Append(") and Type<=1)) OR ");
+ statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.AlbumArtistIds.Length > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var artistId in query.AlbumArtistIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.AlbumArtistIds.Length; i++)
{
- var paramName = "@ArtistIds" + index;
- clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
- statement?.TryBind(paramName, artistId);
- index++;
+ clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
+ .Append(i)
+ .Append(") and Type=1)) OR ");
+ statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.ContributingArtistIds.Length > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var artistId in query.ContributingArtistIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.ContributingArtistIds.Length; i++)
{
- var paramName = "@ArtistIds" + index;
- clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
- statement?.TryBind(paramName, artistId);
- index++;
+ clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds")
+ .Append(i)
+ .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds")
+ .Append(i)
+ .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR ");
+ statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.AlbumIds.Length > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var albumId in query.AlbumIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.AlbumIds.Length; i++)
{
- var paramName = "@AlbumIds" + index;
- clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")");
- statement?.TryBind(paramName, albumId);
- index++;
+ clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds")
+ .Append(i)
+ .Append(") OR ");
+ statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.ExcludeArtistIds.Length > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var artistId in query.ExcludeArtistIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.ExcludeArtistIds.Length; i++)
{
- var paramName = "@ExcludeArtistId" + index;
- clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
- statement?.TryBind(paramName, artistId);
- index++;
+ clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId")
+ .Append(i)
+ .Append(") and Type<=1)) OR ");
+ statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.GenreIds.Count > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var genreId in query.GenreIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.GenreIds.Count; i++)
{
- var paramName = "@GenreId" + index;
- clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
- statement?.TryBind(paramName, genreId);
- index++;
+ clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId")
+ .Append(i)
+ .Append(") and Type=2)) OR ");
+ statement?.TryBind("@GenreId" + i, query.GenreIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.Genres.Count > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var item in query.Genres)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.Genres.Count; i++)
{
- clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
- statement?.TryBind("@Genre" + index, GetCleanValue(item));
- index++;
+ clauseBuilder.Append("@Genre")
+ .Append(i)
+ .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR ");
+ statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i]));
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (tags.Count > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var item in tags)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < tags.Count; i++)
{
- clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
- statement?.TryBind("@Tag" + index, GetCleanValue(item));
- index++;
+ clauseBuilder.Append("@Tag")
+ .Append(i)
+ .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
+ statement?.TryBind("@Tag" + i, GetCleanValue(tags[i]));
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (excludeTags.Count > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var item in excludeTags)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < excludeTags.Count; i++)
{
- clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
- statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item));
- index++;
+ clauseBuilder.Append("@ExcludeTag")
+ .Append(i)
+ .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
+ statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i]));
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.StudioIds.Length > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var studioId in query.StudioIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.StudioIds.Length; i++)
{
- var paramName = "@StudioId" + index;
- clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
- statement?.TryBind(paramName, studioId);
- index++;
+ clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId")
+ .Append(i)
+ .Append(") and Type=3)) OR ");
+ statement?.TryBind("@StudioId" + i, query.StudioIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.OfficialRatings.Length > 0)
{
- var clauses = new List<string>();
- var index = 0;
- foreach (var item in query.OfficialRatings)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.OfficialRatings.Length; i++)
{
- clauses.Add("OfficialRating=@OfficialRating" + index);
- statement?.TryBind("@OfficialRating" + index, item);
- index++;
+ clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or);
+ statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
- var ratingClauseBuilder = new StringBuilder("(");
+ clauseBuilder.Append('(');
if (query.HasParentalRating ?? false)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue not null");
+ clauseBuilder.Append("InheritedParentalRatingValue not null");
if (query.MinParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
+ clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
}
if (query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
+ clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
}
else if (query.BlockUnratedItems.Length > 0)
{
- var paramName = "@UnratedType";
- var index = 0;
- string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++));
- ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))");
+ const string ParamName = "@UnratedType";
+ clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (");
- if (statement is not null)
+ for (int i = 0; i < query.BlockUnratedItems.Length; i++)
{
- for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++)
- {
- statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString());
- }
+ clauseBuilder.Append(ParamName).Append(i).Append(',');
+ statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString());
}
+ // Remove trailing comma
+ clauseBuilder.Length--;
+ clauseBuilder.Append("))");
+
if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" OR (");
+ clauseBuilder.Append(" OR (");
}
if (query.MinParentalRating.HasValue)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
+ clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
}
@@ -4035,50 +3929,50 @@ namespace Emby.Server.Implementations.Data
{
if (query.MinParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" AND ");
+ clauseBuilder.Append(" AND ");
}
- ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
+ clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append(")");
+ clauseBuilder.Append(')');
}
if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue))
{
- ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null");
+ clauseBuilder.Append(" OR InheritedParentalRatingValue not null");
}
}
else if (query.MinParentalRating.HasValue)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
+ clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
if (query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
+ clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
- ratingClauseBuilder.Append(")");
+ clauseBuilder.Append(')');
}
else if (query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
+ clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
else if (!query.HasParentalRating ?? false)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue is null");
+ clauseBuilder.Append("InheritedParentalRatingValue is null");
}
- var ratingClauseString = ratingClauseBuilder.ToString();
- if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase))
+ if (clauseBuilder.Length > 1)
{
- whereClauses.Add(ratingClauseString + ")");
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.HasOfficialRating.HasValue)
@@ -4485,7 +4379,7 @@ namespace Emby.Server.Implementations.Data
foreach (var videoType in query.VideoTypes)
{
- videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'");
+ videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
}
whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");
@@ -4565,7 +4459,6 @@ namespace Emby.Server.Implementations.Data
return whereClauses;
}
-#nullable disable
/// <summary>
/// Formats a where clause for the specified provider.
@@ -4582,6 +4475,7 @@ namespace Emby.Server.Implementations.Data
provider);
}
+#nullable disable
private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
{
var list = new List<string>();
@@ -4661,44 +4555,28 @@ namespace Emby.Server.Implementations.Data
return true;
}
- if (query.IncludeItemTypes.Contains(BaseItemKind.Episode)
+ return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
|| query.IncludeItemTypes.Contains(BaseItemKind.Video)
|| query.IncludeItemTypes.Contains(BaseItemKind.Movie)
|| query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
|| query.IncludeItemTypes.Contains(BaseItemKind.Series)
- || query.IncludeItemTypes.Contains(BaseItemKind.Season))
- {
- return true;
- }
-
- return false;
+ || query.IncludeItemTypes.Contains(BaseItemKind.Season);
}
public void UpdateInheritedValues()
{
- string sql = string.Join(
- ';',
- new string[]
- {
- "delete from ItemValues where type = 6",
-
- "insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
-
- @"insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
+ const string Statements = """
+delete from ItemValues where type = 6;
+insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4;
+insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
FROM AncestorIds
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
-where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
- });
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- connection.ExecuteAll(sql);
- },
- TransactionMode);
- }
+where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4;
+""";
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ connection.Execute(Statements);
+ transaction.Commit();
}
public void DeleteItem(Guid id)
@@ -4710,43 +4588,36 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- Span<byte> idBlob = stackalloc byte[16];
- id.TryWriteBytes(idBlob);
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ // Delete people
+ ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id);
- // Delete people
- ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob);
+ // Delete chapters
+ ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id);
- // Delete chapters
- ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob);
+ // Delete media streams
+ ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id);
- // Delete media streams
- ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob);
+ // Delete ancestors
+ ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id);
- // Delete ancestors
- ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob);
+ // Delete item values
+ ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id);
- // Delete item values
- ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob);
+ // Delete the item
+ ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id);
- // Delete the item
- ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob);
- },
- TransactionMode);
- }
+ transaction.Commit();
}
- private void ExecuteWithSingleParam(IDatabaseConnection db, string query, ReadOnlySpan<byte> value)
+ private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value)
{
using (var statement = PrepareStatement(db, query))
{
statement.TryBind("@Id", value);
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
}
@@ -4773,7 +4644,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
var list = new List<string>();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
@@ -4794,25 +4665,25 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
- var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
+ StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p");
var whereClauses = GetPeopleWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
}
- commandText += " order by ListOrder";
+ commandText.Append(" order by ListOrder");
if (query.Limit > 0)
{
- commandText += " LIMIT " + query.Limit;
+ commandText.Append(" LIMIT ").Append(query.Limit);
}
var list = new List<PersonInfo>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
+ using (var connection = GetConnection())
+ using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
@@ -4826,7 +4697,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return list;
}
- private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement)
+ private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement)
{
var whereClauses = new List<string>();
@@ -4896,7 +4767,7 @@ AND Type = @InternalPersonType)");
return whereClauses;
}
- private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement)
+ private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
{
if (itemId.Equals(default))
{
@@ -4907,13 +4778,9 @@ AND Type = @InternalPersonType)");
CheckDisposed();
- Span<byte> itemIdBlob = stackalloc byte[16];
- itemId.TryWriteBytes(itemIdBlob);
-
// First delete
- deleteAncestorsStatement.Reset();
- deleteAncestorsStatement.TryBind("@ItemId", itemIdBlob);
- deleteAncestorsStatement.MoveNext();
+ deleteAncestorsStatement.TryBind("@ItemId", itemId);
+ deleteAncestorsStatement.ExecuteNonQuery();
if (ancestorIds.Count == 0)
{
@@ -4930,26 +4797,24 @@ AND Type = @InternalPersonType)");
i.ToString(CultureInfo.InvariantCulture));
}
- // Remove last ,
+ // Remove trailing comma
insertText.Length--;
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", itemIdBlob);
+ statement.TryBind("@ItemId", itemId);
for (var i = 0; i < ancestorIds.Count; i++)
{
var index = i.ToString(CultureInfo.InvariantCulture);
var ancestorId = ancestorIds[i];
- ancestorId.TryWriteBytes(itemIdBlob);
- statement.TryBind("@AncestorId" + index, itemIdBlob);
+ statement.TryBind("@AncestorId" + index, ancestorId);
statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
}
@@ -5057,7 +4922,7 @@ AND Type = @InternalPersonType)");
var list = new List<string>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText))
{
foreach (var row in statement.ExecuteQuery())
@@ -5257,77 +5122,75 @@ AND Type = @InternalPersonType)");
var list = new List<(BaseItem, ItemCounts)>();
var result = new QueryResult<(BaseItem, ItemCounts)>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction(deferred: true))
{
- connection.RunInTransaction(
- db =>
+ if (!isReturningZeroItems)
+ {
+ using (var statement = PrepareStatement(connection, commandText))
{
- if (!isReturningZeroItems)
+ statement.TryBind("@SelectType", returnType);
+ if (EnableJoinUserData(query))
{
- using (var statement = PrepareStatement(db, commandText))
- {
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
- if (item is not null)
- {
- var countStartColumn = columns.Count - 1;
-
- list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
- }
- }
- }
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
+
+ if (typeSubQuery is not null)
+ {
+ GetWhereClauses(typeSubQuery, null);
}
- if (query.EnableTotalRecordCount)
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+ GetWhereClauses(innerQuery, statement);
+ GetWhereClauses(outerQuery, statement);
+
+ var hasEpisodeAttributes = HasEpisodeAttributes(query);
+ var hasProgramAttributes = HasProgramAttributes(query);
+ var hasServiceName = HasServiceName(query);
+ var hasStartDate = HasStartDate(query);
+ var hasTrailerTypes = HasTrailerTypes(query);
+ var hasArtistFields = HasArtistFields(query);
+ var hasSeriesFields = HasSeriesFields(query);
+
+ foreach (var row in statement.ExecuteQuery())
{
- using (var statement = PrepareStatement(db, countText))
+ var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+ if (item is not null)
{
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
+ var countStartColumn = columns.Count - 1;
+
+ list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
}
}
- },
- ReadTransactionMode);
+ }
+ }
+
+ if (query.EnableTotalRecordCount)
+ {
+ using (var statement = PrepareStatement(connection, countText))
+ {
+ statement.TryBind("@SelectType", returnType);
+ if (EnableJoinUserData(query))
+ {
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
+
+ if (typeSubQuery is not null)
+ {
+ GetWhereClauses(typeSubQuery, null);
+ }
+
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+ GetWhereClauses(innerQuery, statement);
+ GetWhereClauses(outerQuery, statement);
+
+ result.TotalRecordCount = statement.SelectScalarInt();
+ }
+ }
+
+ transaction.Commit();
}
if (result.TotalRecordCount == 0)
@@ -5341,7 +5204,7 @@ AND Type = @InternalPersonType)");
return result;
}
- private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, BaseItemKind[] typesToCount)
+ private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount)
{
var counts = new ItemCounts();
@@ -5420,7 +5283,7 @@ AND Type = @InternalPersonType)");
return list;
}
- private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, IDatabaseConnection db)
+ private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
{
if (itemId.Equals(default))
{
@@ -5431,15 +5294,15 @@ AND Type = @InternalPersonType)");
CheckDisposed();
- var guidBlob = itemId.ToByteArray();
-
// First delete
- db.Execute("delete from ItemValues where ItemId=@Id", guidBlob);
+ using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id");
+ command.TryBind("@Id", itemId);
+ command.ExecuteNonQuery();
- InsertItemValues(guidBlob, values, db);
+ InsertItemValues(itemId, values, db);
}
- private void InsertItemValues(byte[] idBlob, List<(int MagicNumber, string Value)> values, IDatabaseConnection db)
+ private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -5458,12 +5321,12 @@ AND Type = @InternalPersonType)");
i);
}
- // Remove last comma
+ // Remove trailing comma
insertText.Length--;
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", idBlob);
+ statement.TryBind("@ItemId", id);
for (var i = startIndex; i < endIndex; i++)
{
@@ -5484,8 +5347,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
startIndex += Limit;
@@ -5504,23 +5366,20 @@ AND Type = @InternalPersonType)");
CheckDisposed();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- var itemIdBlob = itemId.ToByteArray();
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ // First delete chapters
+ using var command = connection.CreateCommand();
+ command.CommandText = "delete from People where ItemId=@ItemId";
+ command.TryBind("@ItemId", itemId);
+ command.ExecuteNonQuery();
- // First delete chapters
- db.Execute("delete from People where ItemId=@ItemId", itemIdBlob);
+ InsertPeople(itemId, people, connection);
- InsertPeople(itemIdBlob, people, db);
- },
- TransactionMode);
- }
+ transaction.Commit();
}
- private void InsertPeople(byte[] idBlob, List<PersonInfo> people, IDatabaseConnection db)
+ private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -5539,12 +5398,12 @@ AND Type = @InternalPersonType)");
i.ToString(CultureInfo.InvariantCulture));
}
- // Remove last comma
+ // Remove trailing comma
insertText.Length--;
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", idBlob);
+ statement.TryBind("@ItemId", id);
for (var i = startIndex; i < endIndex; i++)
{
@@ -5561,8 +5420,7 @@ AND Type = @InternalPersonType)");
listIndex++;
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
startIndex += Limit;
@@ -5570,7 +5428,7 @@ AND Type = @InternalPersonType)");
}
}
- private PersonInfo GetPerson(IReadOnlyList<ResultSetValue> reader)
+ private PersonInfo GetPerson(SqliteDataReader reader)
{
var item = new PersonInfo
{
@@ -5617,7 +5475,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by StreamIndex ASC";
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
{
var list = new List<MediaStream>();
@@ -5658,23 +5516,19 @@ AND Type = @InternalPersonType)");
cancellationToken.ThrowIfCancellationRequested();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- var itemIdBlob = id.ToByteArray();
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ // Delete existing mediastreams
+ using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId");
+ command.TryBind("@ItemId", id);
+ command.ExecuteNonQuery();
- // Delete existing mediastreams
- db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
+ InsertMediaStreams(id, streams, connection);
- InsertMediaStreams(itemIdBlob, streams, db);
- },
- TransactionMode);
- }
+ transaction.Commit();
}
- private void InsertMediaStreams(byte[] idBlob, IReadOnlyList<MediaStream> streams, IDatabaseConnection db)
+ private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db)
{
const int Limit = 10;
var startIndex = 0;
@@ -5706,7 +5560,7 @@ AND Type = @InternalPersonType)");
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", idBlob);
+ statement.TryBind("@ItemId", id);
for (var i = startIndex; i < endIndex; i++)
{
@@ -5742,6 +5596,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@PixelFormat" + index, stream.PixelFormat);
statement.TryBind("@BitDepth" + index, stream.BitDepth);
+ statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic);
statement.TryBind("@IsExternal" + index, stream.IsExternal);
statement.TryBind("@RefFrames" + index, stream.RefFrames);
@@ -5770,8 +5625,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired);
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
startIndex += Limit;
@@ -5784,15 +5638,14 @@ AND Type = @InternalPersonType)");
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>MediaStream.</returns>
- private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
+ private MediaStream GetMediaStream(SqliteDataReader reader)
{
var item = new MediaStream
{
- Index = reader[1].ToInt()
+ Index = reader.GetInt32(1),
+ Type = Enum.Parse<MediaStreamType>(reader.GetString(2), true)
};
- item.Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true);
-
if (reader.TryGetString(3, out var codec))
{
item.Codec = codec;
@@ -5979,7 +5832,7 @@ AND Type = @InternalPersonType)");
item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
}
- item.IsHearingImpaired = reader.GetBoolean(43);
+ item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
if (item.Type == MediaStreamType.Subtitle)
{
@@ -6009,10 +5862,10 @@ AND Type = @InternalPersonType)");
cmdText += " order by AttachmentIndex ASC";
var list = new List<MediaAttachment>();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, cmdText))
{
- statement.TryBind("@ItemId", query.ItemId.ToByteArray());
+ statement.TryBind("@ItemId", query.ItemId);
if (query.Index.HasValue)
{
@@ -6044,24 +5897,22 @@ AND Type = @InternalPersonType)");
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction())
+ using (var command = connection.PrepareStatement("delete from mediaattachments where ItemId=@ItemId"))
{
- connection.RunInTransaction(
- db =>
- {
- var itemIdBlob = id.ToByteArray();
+ command.TryBind("@ItemId", id);
+ command.ExecuteNonQuery();
- db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob);
+ InsertMediaAttachments(id, attachments, connection, cancellationToken);
- InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken);
- },
- TransactionMode);
+ transaction.Commit();
}
}
private void InsertMediaAttachments(
- byte[] idBlob,
+ Guid id,
IReadOnlyList<MediaAttachment> attachments,
- IDatabaseConnection db,
+ SqliteConnection db,
CancellationToken cancellationToken)
{
const int InsertAtOnce = 10;
@@ -6073,14 +5924,13 @@ AND Type = @InternalPersonType)");
for (var i = startIndex; i < endIndex; i++)
{
- var index = i.ToString(CultureInfo.InvariantCulture);
insertText.Append("(@ItemId, ");
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
{
insertText.Append('@')
.Append(column)
- .Append(index)
+ .Append(i)
.Append(',');
}
@@ -6095,7 +5945,7 @@ AND Type = @InternalPersonType)");
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", idBlob);
+ statement.TryBind("@ItemId", id);
for (var i = startIndex; i < endIndex; i++)
{
@@ -6111,8 +5961,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@MIMEType" + index, attachment.MimeType);
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
insertText.Length = _mediaAttachmentInsertPrefix.Length;
@@ -6124,11 +5973,11 @@ AND Type = @InternalPersonType)");
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>MediaAttachment.</returns>
- private MediaAttachment GetMediaAttachment(IReadOnlyList<ResultSetValue> reader)
+ private MediaAttachment GetMediaAttachment(SqliteDataReader reader)
{
var item = new MediaAttachment
{
- Index = reader[1].ToInt()
+ Index = reader.GetInt32(1)
};
if (reader.TryGetString(2, out var codec))
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index a1e217ad1..a5edcc58c 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
@@ -44,48 +44,48 @@ namespace Emby.Server.Implementations.Data
var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : _userManager.Users;
+ using var transaction = connection.BeginTransaction();
+ connection.Execute(string.Join(
+ ';',
+ "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",
+ "drop index if exists idx_userdata1",
+ "drop index if exists idx_userdata2",
+ "drop index if exists userdataindex1",
+ "drop index if exists userdataindex",
+ "drop index if exists userdataindex3",
+ "drop index if exists userdataindex4",
+ "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
+ "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
+ "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
+ "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
+
+ if (!userDataTableExists)
+ {
+ transaction.Commit();
+ return;
+ }
- connection.RunInTransaction(
- db =>
- {
- 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",
- "drop index if exists idx_userdata1",
- "drop index if exists idx_userdata2",
- "drop index if exists userdataindex1",
- "drop index if exists userdataindex",
- "drop index if exists userdataindex3",
- "drop index if exists userdataindex4",
- "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
- "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
- "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
- "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
- }));
-
- if (userDataTableExists)
- {
- var existingColumnNames = GetColumnNames(db, "userdata");
-
- AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
- AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
- AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
-
- if (!userDatasTableExists)
- {
- ImportUserIds(db, users);
-
- db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
- }
- }
- },
- TransactionMode);
+ var existingColumnNames = GetColumnNames(connection, "userdata");
+
+ AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
+ AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
+ AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
+
+ if (userDatasTableExists)
+ {
+ return;
+ }
+
+ ImportUserIds(connection, users);
+
+ connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
+
+ transaction.Commit();
}
}
- private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users)
+ private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data
statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId);
- statement.MoveNext();
- statement.Reset();
+ statement.ExecuteNonQuery();
}
}
}
- private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db)
+ private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
{
var list = new List<Guid>();
@@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data
{
try
{
- list.Add(row[0].ReadGuidFromBlob());
+ list.Add(row.GetGuid(0));
}
catch (Exception ex)
{
@@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction())
{
- connection.RunInTransaction(
- db =>
- {
- SaveUserData(db, internalUserId, key, userData);
- },
- TransactionMode);
+ SaveUserData(connection, internalUserId, key, userData);
+ transaction.Commit();
}
}
- private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData)
+ private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
{
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{
@@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data
statement.TryBindNull("@SubtitleStreamIndex");
}
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
}
@@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction())
{
- connection.RunInTransaction(
- db =>
- {
- foreach (var userItemData in userDataList)
- {
- SaveUserData(db, internalUserId, userItemData.Key, userItemData);
- }
- },
- TransactionMode);
+ foreach (var userItemData in userDataList)
+ {
+ SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
+ }
+
+ transaction.Commit();
}
}
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data
ArgumentException.ThrowIfNullOrEmpty(key);
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
@@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns>
- private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
+ private UserItemData ReadRow(SqliteDataReader reader)
{
var userData = new UserItemData();
@@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data
userData.Rating = rating;
}
- userData.Played = reader[3].ToBool();
- userData.PlayCount = reader[4].ToInt();
- userData.IsFavorite = reader[5].ToBool();
- userData.PlaybackPositionTicks = reader[6].ToInt64();
+ userData.Played = reader.GetBoolean(3);
+ userData.PlayCount = reader.GetInt32(4);
+ userData.IsFavorite = reader.GetBoolean(5);
+ userData.PlaybackPositionTicks = reader.GetInt64(6);
if (reader.TryReadDateTime(7, out var lastPlayedDate))
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 7a6ed2cb8..44b97e8b8 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ILyricManager _lyricManager;
+ private readonly ITrickplayManager _trickplayManager;
public DtoService(
ILogger<DtoService> logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory,
- ILyricManager lyricManager)
+ ILyricManager lyricManager,
+ ITrickplayManager trickplayManager)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_lyricManager = lyricManager;
+ _trickplayManager = trickplayManager;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -903,10 +907,11 @@ namespace Emby.Server.Implementations.Dto
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
}
+ dto.LUFS = item.LUFS;
+
// Add audio info
if (item is Audio audio)
{
- dto.LUFS = audio.LUFS;
dto.Album = audio.Album;
if (audio.ExtraType.HasValue)
{
@@ -1058,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto
dto.Chapters = _itemRepo.GetChapters(item);
}
+ if (options.ContainsField(ItemFields.Trickplay))
+ {
+ dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
+ }
+
if (video.ExtraType.HasValue)
{
dto.ExtraType = video.ExtraType.Value.ToString();
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index b8655c760..b48e389ac 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -24,6 +24,7 @@
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Jellyfin.XmlTv" />
+ <PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
@@ -31,7 +32,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Mono.Nat" />
<PackageReference Include="prometheus-net.DotNetRuntime" />
- <PackageReference Include="SQLitePCL.pretty.netstandard" />
<PackageReference Include="DotNet.Glob" />
</ItemGroup>
@@ -43,16 +43,19 @@
<TargetFramework>net7.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
- <NoWarn>AD0001</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <!-- TODO: Add IDisposableAnalyzers -->
+ <!-- <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference> -->
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 54191649d..7e4994f1a 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
/// <summary>
- /// Class UdpServerEntryPoint.
+ /// Class responsible for registering all UDP broadcast endpoints and their handlers.
/// </summary>
public sealed class UdpServerEntryPoint : IServerEntryPoint
{
@@ -35,14 +35,13 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager;
private readonly INetworkManager _networkManager;
- private readonly bool _enableMultiSocketBinding;
/// <summary>
/// The UDP server.
/// </summary>
- private List<UdpServer> _udpServers;
- private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
- private bool _disposed = false;
+ private readonly List<UdpServer> _udpServers;
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
@@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
_configurationManager = configurationManager;
_networkManager = networkManager;
_udpServers = new List<UdpServer>();
- _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
}
/// <inheritdoc />
@@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- if (_enableMultiSocketBinding)
+ // Linux needs to bind to the broadcast addresses to get broadcast traffic
+ // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
+ if (OperatingSystem.IsLinux())
{
- // Add global broadcast socket
+ // Add global broadcast listener
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server);
- // Add bind address specific broadcast sockets
+ // Add bind address specific broadcast listeners
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces)
@@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints
}
else
{
- var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber);
- server.Start(_cancellationTokenSource.Token);
- _udpServers.Add(server);
+ // Add bind address specific broadcast listeners
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+ foreach (var intf in validInterfaces)
+ {
+ var intfAddress = intf.Address;
+ _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
+
+ var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
+ server.Start(_cancellationTokenSource.Token);
+ _udpServers.Add(server);
+ }
}
}
catch (SocketException ex)
@@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (_disposed)
{
- throw new ObjectDisposedException(this.GetType().Name);
+ throw new ObjectDisposedException(GetType().Name);
}
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7f620d666..f83da566b 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 0ad81b653..e75cab64c 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.IO
}
}
- public void ResetPath(string path, string affectedFile)
+ public void ResetPath(string path, string? affectedFile)
{
lock (_timerLock)
{
@@ -148,13 +148,6 @@ namespace Emby.Server.Implementations.IO
{
item.ChangedExternally();
}
- catch (IOException ex)
- {
- // For now swallow and log.
- // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
- // Should we remove it from it's parent?
- _logger.LogError(ex, "Error refreshing {Name}", item.Name);
- }
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
@@ -217,7 +210,6 @@ namespace Emby.Server.Implementations.IO
DisposeTimer();
_disposed = true;
- GC.SuppressFinalize(this);
}
}
}
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index f67a02be8..dde38906f 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -160,7 +158,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
+ private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
+ private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO
/// <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"><paramref name="path"/> is <c>null</c>.</exception>
- private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
+ private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
{
- ArgumentException.ThrowIfNullOrEmpty(path);
+ if (path.IsEmpty)
+ {
+ throw new ArgumentException("Path can't be empty", nameof(path));
+ }
path = path.TrimEnd(Path.DirectorySeparatorChar);
- return lst.Any(str =>
+ foreach (var str in lst)
{
// this should be a little quicker than examining each actual parent folder...
- var compare = str.TrimEnd(Path.DirectorySeparatorChar);
+ var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
- return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
- });
+ if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
+ || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar))
+ {
+ return true;
+ }
+ }
+
+ return false;
}
/// <summary>
@@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO
{
ArgumentException.ThrowIfNullOrEmpty(path);
- var monitorPath = !IgnorePatterns.ShouldIgnore(path);
+ if (IgnorePatterns.ShouldIgnore(path))
+ {
+ return;
+ }
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
- if (_tempIgnoredPaths.Keys.Any(i =>
+ foreach (var i in _tempIgnoredPaths.Keys)
{
- if (_fileSystem.AreEqual(i, path))
- {
- _logger.LogDebug("Ignoring change to {Path}", path);
- return true;
- }
-
- if (_fileSystem.ContainsSubPath(i, path))
+ if (_fileSystem.AreEqual(i, path)
+ || _fileSystem.ContainsSubPath(i, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
- return true;
+ return;
}
// Go up a level
@@ -371,20 +376,11 @@ namespace Emby.Server.Implementations.IO
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
- return true;
+ return;
}
-
- return false;
- }))
- {
- monitorPath = false;
}
- if (monitorPath)
- {
- // Avoid implicitly captured closure
- CreateRefresher(path);
- }
+ CreateRefresher(path);
}
private void CreateRefresher(string path)
@@ -417,7 +413,8 @@ namespace Emby.Server.Implementations.IO
}
// They are siblings. Rebase the refresher to the parent folder.
- if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal))
+ if (parentPath is not null
+ && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
{
refresher.ResetPath(parentPath, path);
return;
@@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO
}
}
- private void OnNewRefresherCompleted(object sender, EventArgs e)
+ private void OnNewRefresherCompleted(object? sender, EventArgs e)
{
+ if (sender is null)
+ {
+ return;
+ }
+
var refresher = (FileRefresher)sender;
DisposeRefresher(refresher);
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 0ba4a488b..c380d67db 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
- private readonly ILogger<ManagedFileSystem> _logger;
-
- private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
- private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
private static readonly char[] _invalidPathCharacters =
{
@@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO
(char)31, ':', '*', '?', '\\', '/'
};
+ private readonly ILogger<ManagedFileSystem> _logger;
+ private readonly List<IShortcutHandler> _shortcutHandlers;
+ private readonly string _tempPath;
+
/// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
+ /// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
- IApplicationPaths applicationPaths)
+ IApplicationPaths applicationPaths,
+ IEnumerable<IShortcutHandler> shortcutHandlers)
{
_logger = logger;
_tempPath = applicationPaths.TempDirectory;
- }
-
- /// <inheritdoc />
- public virtual void AddShortcutHandler(IShortcutHandler handler)
- {
- _shortcutHandlers.Add(handler);
+ _shortcutHandlers = shortcutHandlers.ToList();
}
/// <summary>
@@ -94,7 +91,7 @@ namespace Emby.Server.Implementations.IO
}
// unc path
- if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
+ if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
{
return filePath;
}
@@ -106,15 +103,17 @@ namespace Emby.Server.Implementations.IO
return filePath;
}
+ var filePathSpan = filePath.AsSpan();
+
// relative path
if (firstChar == '\\')
{
- filePath = filePath.Substring(1);
+ filePathSpan = filePathSpan.Slice(1);
}
try
{
- return Path.GetFullPath(Path.Combine(folderPath, filePath));
+ return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
}
catch (ArgumentException)
{
@@ -486,24 +485,10 @@ namespace Emby.Server.Implementations.IO
}
/// <inheritdoc />
- public virtual string NormalizePath(string path)
- {
- ArgumentException.ThrowIfNullOrEmpty(path);
-
- if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
- {
- return path;
- }
-
- return Path.TrimEndingDirectorySeparator(path);
- }
-
- /// <inheritdoc />
public virtual bool AreEqual(string path1, string path2)
{
- return string.Equals(
- NormalizePath(path1),
- NormalizePath(path2),
+ return Path.TrimEndingDirectorySeparator(path1).Equals(
+ Path.TrimEndingDirectorySeparator(path2),
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
index c2aab3879..5776c7a7c 100644
--- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
+++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
@@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO
{
public class MbLinkShortcutHandler : IShortcutHandler
{
- private readonly IFileSystem _fileSystem;
-
- public MbLinkShortcutHandler(IFileSystem fileSystem)
- {
- _fileSystem = fileSystem;
- }
-
public string Extension => ".mblink";
public string? Resolve(string shortcutPath)
{
ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
- if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase))
+ if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase))
{
var path = File.ReadAllText(shortcutPath);
- return _fileSystem.NormalizePath(path);
+ return Path.TrimEndingDirectorySeparator(path);
}
return null;
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index 5384c04b3..cf6fc1845 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
// bts sync files
"**/*.bts",
"**/*.sync",
+
+ // zfs
+ "**/.zfs/**",
+ "**/.zfs"
};
private static readonly GlobOptions _globOptions = new GlobOptions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 8bb2d3c02..4f0983564 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -45,7 +46,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger;
- private readonly IMemoryCache _memoryCache;
+ private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@@ -111,7 +111,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
- /// <param name="memoryCache">The memory cache.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
public LibraryManager(
@@ -128,7 +127,6 @@ namespace Emby.Server.Implementations.Library
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor,
- IMemoryCache memoryCache,
NamingOptions namingOptions,
IDirectoryService directoryService)
{
@@ -145,7 +143,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
- _memoryCache = memoryCache;
+ _cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
@@ -300,7 +298,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _memoryCache.Set(item.Id, item);
+ _cache[item.Id] = item;
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -359,7 +357,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
- : Enumerable.Empty<BaseItem>();
+ : Array.Empty<BaseItem>();
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@@ -441,7 +439,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id);
}
- _memoryCache.Remove(item.Id);
+ _cache.TryRemove(item.Id, out _);
ReportItemRemoved(item, parent);
}
@@ -609,7 +607,7 @@ namespace Emby.Server.Implementations.Library
var originalList = paths.ToList();
var list = originalList.Where(i => i.IsDirectory)
- .Select(i => _fileSystem.NormalizePath(i.FullName))
+ .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
@@ -840,19 +838,12 @@ namespace Emby.Server.Implementations.Library
{
var path = Person.GetPath(name);
var id = GetItemByNameId<Person>(path);
- if (GetItemById(id) is not Person item)
+ if (GetItemById(id) is Person item)
{
- item = new Person
- {
- Name = name,
- Id = id,
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
- Path = path
- };
+ return item;
}
- return item;
+ return null;
}
/// <summary>
@@ -1163,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
Name = Path.GetFileName(dir),
Locations = _fileSystem.GetFilePaths(dir, false)
- .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.Select(i =>
{
try
@@ -1233,7 +1224,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_memoryCache.TryGetValue(id, out BaseItem item))
+ if (_cache.TryGetValue(id, out BaseItem item))
{
return item;
}
@@ -2859,7 +2850,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
- File.WriteAllBytes(path, Array.Empty<byte>());
+ await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2901,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
var saveEntity = false;
var personEntity = GetPerson(person.Name);
- // if PresentationUniqueKey is empty it's likely a new item.
- if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+ if (personEntity is null)
{
+ var path = Person.GetPath(person.Name);
+ personEntity = new Person()
+ {
+ Name = person.Name,
+ Id = GetItemByNameId<Person>(path),
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
+ Path = path
+ };
+
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
}
@@ -3136,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
}
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
- .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(shortcut))
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 936a08da8..59d705ace 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
+ FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
- await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
- catch
+ catch (Exception ex)
{
+ _logger.LogError(ex, "Error deserializing mediainfo cache");
+ }
+ finally
+ {
+ await jsonStream.DisposeAsync().ConfigureAwait(false);
}
}
@@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
- await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
- // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
+ _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index c9a26a30f..91469dba9 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
+ FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
- await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
- // _logger.LogDebug("Found cached media info");
}
catch (Exception ex)
{
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
}
+ finally
+ {
+ await jsonStream.DisposeAsync().ConfigureAwait(false);
+ }
}
if (mediaInfo is null)
@@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = File.Create(cacheFilePath);
- await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream createStream = File.Create(cacheFilePath);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index a74f82475..862f144e6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
{
- var extension = Path.GetExtension(args.Path);
+ var extension = Path.GetExtension(args.Path.AsSpan());
- if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
+ if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
{
// if audio file exists of same name, return null
return null;
@@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (item is not null)
{
- item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+ item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
item.IsInMixedFolder = true;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 381796d0e..779cfd5be 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false;
}
- return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
+ return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 042422c6f..73861ff59 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
return GetBook(args);
}
- var extension = Path.GetExtension(args.Path);
+ var extension = Path.GetExtension(args.Path.AsSpan());
- if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's a book
return new Book
@@ -51,12 +51,11 @@ 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.AsSpan());
return _validExtensions.Contains(
fileExtension,
- StringComparer.OrdinalIgnoreCase);
+ StringComparison.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/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index 9026160ff..c860391fc 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,7 +1,4 @@
-#nullable disable
-
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
@@ -25,7 +22,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
- private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly string[] _ignoreFiles = new[]
{
"folder",
"thumb",
@@ -56,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Trailer.</returns>
- protected override Photo Resolve(ItemResolveArgs args)
+ protected override Photo? Resolve(ItemResolveArgs args)
{
if (!args.IsDirectory)
{
@@ -68,10 +65,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsImageFile(args.Path, _imageProcessor))
{
- var filename = Path.GetFileNameWithoutExtension(args.Path);
+ var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
// Make sure the image doesn't belong to a video file
- var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
+ var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
+ ?? throw new InvalidOperationException("Path can't be a root directory."));
foreach (var file in files)
{
@@ -92,32 +90,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
- internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
+ internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
{
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
}
- internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
+ internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{
ArgumentNullException.ThrowIfNull(path);
- var filename = Path.GetFileNameWithoutExtension(path);
-
- if (_ignoreFiles.Contains(filename))
+ var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
+ if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
- if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
+ var filename = Path.GetFileNameWithoutExtension(path);
+
+ if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
- string extension = Path.GetExtension(path).TrimStart('.');
- return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
+ return true;
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index e9538a5c9..858c5b281 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var resolver = new Naming.TV.EpisodeResolver(namingOptions);
var folderName = System.IO.Path.GetFileName(path);
- var testPath = "\\\\test\\" + folderName;
+ var testPath = @"\\test\" + folderName;
var episodeInfo = resolver.Resolve(testPath, true);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index b9d0f170a..74b62ca3f 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ await using (stream.ConfigureAwait(false))
{
var settings = new XmlWriterSettings
{
@@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Async = true
};
- await using (var writer = XmlWriter.Create(stream, settings))
+ var writer = XmlWriter.Create(stream, settings);
+ await using (writer.ConfigureAwait(false))
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
@@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ await using (stream.ConfigureAwait(false))
{
var settings = new XmlWriterSettings
{
@@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var isSeriesEpisode = timer.IsProgramSeries;
- await using (var writer = XmlWriter.Create(stream, settings))
+ var writer = XmlWriter.Create(stream, settings);
+ await using (writer.ConfigureAwait(false))
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
@@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
else
{
- await writer.WriteStartElementAsync(null, "movie", null);
+ await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(item.Name))
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 7645c6c52..6b0520ad0 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = JsonContent.Create(requestList, options: _jsonOptions);
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<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null)
{
return Array.Empty<ProgramInfo>();
@@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
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<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (programDetails is null)
{
return Array.Empty<ProgramInfo>();
@@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
- await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
- await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
+ var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is not null)
{
foreach (HeadendsDto headend in root)
@@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
@@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var response = httpResponse.Content;
- var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
+ var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
}
catch (HttpRequestException ex)
@@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
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);
+ var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is null)
{
return new List<ChannelInfo>();
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 7b6c8b80a..ff25ee585 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -16,21 +17,20 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
- private readonly IMemoryCache _memoryCache;
+ private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache;
- protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
+ protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem)
{
Config = config;
Logger = logger;
- _memoryCache = memoryCache;
FileSystem = fileSystem;
+ _cache = new ConcurrentDictionary<string, List<ChannelInfo>>();
}
protected IServerConfigurationManager Config { get; }
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var key = tuner.Id;
- if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
+ if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache))
{
return cache;
}
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!string.IsNullOrEmpty(key) && list.Count > 0)
{
- _memoryCache.Set(key, list);
+ _cache[key] = list;
}
return list;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 1795e85a3..8cd0c4ffb 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
- IStreamHelper streamHelper,
- IMemoryCache memoryCache)
- : base(config, logger, fileSystem, memoryCache)
+ IStreamHelper streamHelper)
+ : base(config, logger, fileSystem)
{
_httpClientFactory = httpClientFactory;
_appHost = appHost;
@@ -77,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
- .ConfigureAwait(false) ?? new List<Channels>();
-
+ var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>();
if (info.ImportFavoritesOnly)
{
- lineup = lineup.Where(i => i.Favorite).ToList();
+ lineup = lineup.Where(i => i.Favorite);
}
return lineup.Where(i => !i.DRM).ToList();
@@ -130,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
- .ConfigureAwait(false);
+ var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey))
{
@@ -176,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List<LiveTvTunerInfo>();
- await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
{
- string stripedLine = StripXML(line);
- if (stripedLine.Contains("Channel", StringComparison.Ordinal))
+ using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+ await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{
- LiveTvTunerStatus status;
- var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
- var name = stripedLine.Substring(0, index - 1);
- var currentChannel = stripedLine.Substring(index + 7);
- if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+ string stripedLine = StripXML(line);
+ if (stripedLine.Contains("Channel", StringComparison.Ordinal))
{
- status = LiveTvTunerStatus.LiveTv;
- }
- else
- {
- status = LiveTvTunerStatus.Available;
- }
+ LiveTvTunerStatus status;
+ var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = stripedLine.Substring(0, index - 1);
+ var currentChannel = stripedLine.Substring(index + 7);
+ if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+ {
+ status = LiveTvTunerStatus.LiveTv;
+ }
+ else
+ {
+ status = LiveTvTunerStatus.Available;
+ }
- tuners.Add(new LiveTvTunerInfo
- {
- Name = name,
- SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
- ProgramName = currentChannel,
- Status = status
- });
+ tuners.Add(new LiveTvTunerInfo
+ {
+ Name = name,
+ SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+ ProgramName = currentChannel,
+ Status = status
+ });
+ }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index a8b090635..68383a554 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -44,8 +44,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult();
}
}
-
- GC.SuppressFinalize(this);
}
public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index acf3964c8..db5e81df5 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -54,9 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
INetworkManager networkManager,
- IStreamHelper streamHelper,
- IMemoryCache memoryCache)
- : base(config, logger, fileSystem, memoryCache)
+ IStreamHelper streamHelper)
+ : base(config, logger, fileSystem)
{
_httpClientFactory = httpClientFactory;
_appHost = appHost;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index df9101f48..341782d9d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
- if (string.IsNullOrWhiteSpace(channel.Id))
- {
- channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
- else
- {
- channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
+ channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
channel.Path = trimmedLine;
channels.Add(channel);
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 0e27dafe1..93d50e6e3 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -16,7 +16,7 @@
"Folders": "المجلدات",
"Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم",
- "HeaderContinueWatching": "أستئناف المشاهدة",
+ "HeaderContinueWatching": "استئناف المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json
index 0967ef424..7c7dd26e9 100644
--- a/Emby.Server.Implementations/Localization/Core/as.json
+++ b/Emby.Server.Implementations/Localization/Core/as.json
@@ -1 +1,43 @@
-{}
+{
+ "Albums": "এলবাম",
+ "Application": "আবেদন",
+ "AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}",
+ "Artists": "শিল্পী",
+ "Channels": "চেনেলস",
+ "Default": "ডিফল্ট",
+ "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
+ "Books": "পুস্তক",
+ "Movies": "চলচ্চিত্ৰ",
+ "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
+ "Collections": "সংগ্রহ",
+ "HeaderFavoriteShows": "প্রিয় শোসমূহ",
+ "Latest": "শেহতীয়া",
+ "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
+ "MixedContent": "মিশ্ৰিত সমগ্ৰতা",
+ "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
+ "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
+ "External": "বাহ্যিক",
+ "Favorites": "পছন্দসই",
+ "Folders": "ফোল্ডাৰ",
+ "Forced": "বলপূর্বক",
+ "Genres": "শ্রেণী",
+ "HeaderAlbumArtists": "অ্যালবাম শিল্পী",
+ "HeaderContinueWatching": "দেখা চালিয়ে যান",
+ "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
+ "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
+ "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
+ "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
+ "HeaderFavoriteSongs": "প্ৰিয় গীত",
+ "HeaderLiveTV": "প্ৰতিবেদন টিভি",
+ "HeaderNextUp": "পৰৱৰ্তী অংশ",
+ "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
+ "HearingImpaired": "শ্ৰবণ অক্ষম",
+ "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
+ "Inherit": "উত্তপ্ত কৰা",
+ "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
+ "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
+ "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
+ "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
+ "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল",
+ "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json
new file mode 100644
index 000000000..85d1f4c88
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/chr.json
@@ -0,0 +1,52 @@
+{
+ "ChapterNameValue": "Didanedi {0}",
+ "HeaderAlbumArtists": "Didanidanolisgisgi",
+ "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
+ "HeaderLiveTV": "Anigadi didanidisgosgi",
+ "HeaderRecordingGroups": "Didanisquodiisgisgi",
+ "HomeVideos": "Diganadi dinagadisgisgi",
+ "Inherit": "Anigwe",
+ "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
+ "MixedContent": "Ganinidi dininoladisgisgi",
+ "Movies": "Anidvnisgisgi",
+ "MusicVideos": "Danodisgisgi didanidisgosgi",
+ "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
+ "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
+ "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
+ "Albums": "Anigawidaniyv",
+ "Application": "Didanvyi",
+ "Artists": "Dinidaniyi",
+ "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
+ "Books": "Didanedi",
+ "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
+ "Channels": "Diganadasgi",
+ "Collections": "Diganadisgi",
+ "Default": "Dinadi",
+ "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
+ "External": "Amohdi",
+ "Favorites": "Nvdayelvdisgi",
+ "Folders": "Didanididisgi",
+ "Forced": "Ganedi",
+ "Genres": "Diganadisgi",
+ "HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
+ "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
+ "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
+ "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
+ "HeaderFavoriteSongs": "Dvganidi danodisgisgi",
+ "HeaderNextUp": "Anidvli uwodoli",
+ "HearingImpaired": "Anitsunidi talunidisgisgi",
+ "ItemAddedWithName": "{0} Dinigwe anididanidisgi",
+ "Latest": "Uwodoli",
+ "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
+ "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
+ "Music": "Danodisgisgi",
+ "NameSeasonUnknown": "Tsunita anidvdisgi",
+ "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
+ "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi",
+ "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi",
+ "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi",
+ "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi",
+ "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi",
+ "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi",
+ "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 08db5a30e..5da33febe 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -22,7 +22,7 @@
"HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbená hudba",
- "HeaderLiveTV": "Televize",
+ "HeaderLiveTV": "Živý přenos",
"HeaderNextUp": "Další díly",
"HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domácí videa",
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
"TaskKeyframeExtractor": "Vytahovač klíčových snímků",
"External": "Externí",
- "HearingImpaired": "Sluchově postižení"
+ "HearingImpaired": "Sluchově postižení",
+ "TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno."
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 1b6eecdcf..837172a5b 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -15,13 +15,13 @@
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
- "HeaderAlbumArtists": "Albums kunstnere",
+ "HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
- "HeaderFavoriteAlbums": "Favorit albummer",
- "HeaderFavoriteArtists": "Favorit kunstnere",
- "HeaderFavoriteEpisodes": "Favorit afsnit",
- "HeaderFavoriteShows": "Favorit serier",
- "HeaderFavoriteSongs": "Favorit sange",
+ "HeaderFavoriteAlbums": "Favoritalbummer",
+ "HeaderFavoriteArtists": "Favoritkunstnere",
+ "HeaderFavoriteEpisodes": "Yndlingsafsnit",
+ "HeaderFavoriteShows": "Yndlingsserier",
+ "HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
@@ -34,8 +34,8 @@
"Latest": "Seneste",
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
- "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
+ "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
@@ -51,7 +51,7 @@
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
- "NotificationOptionPluginError": "Plugin fejl",
+ "NotificationOptionPluginError": "Plugin-fejl",
"NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@@ -92,26 +92,26 @@
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
- "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
+ "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
- "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
- "TaskCleanLogs": "Ryd Log mappe",
- "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
- "TaskRefreshLibrary": "Scan Medie Bibliotek",
- "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
- "TaskCleanCache": "Ryd Cache mappe",
- "TasksChannelsCategory": "Internet Kanaler",
+ "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
+ "TaskCleanLogs": "Ryd Log-mappe",
+ "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
+ "TaskRefreshLibrary": "Scan Mediebibliotek",
+ "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
+ "TaskCleanCache": "Ryd Cache-mappe",
+ "TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse",
- "TaskRefreshChapterImages": "Udtræk kapitel billeder",
- "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
- "TaskRefreshChannelsDescription": "Opdater internet kanal information.",
+ "TaskRefreshChapterImages": "Udtræk kapitelbilleder",
+ "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
+ "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
"TaskRefreshChannels": "Opdater Kanaler",
- "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
- "TaskCleanTranscode": "Tøm Transcode mappen",
+ "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
+ "TaskCleanTranscode": "Tøm Transcode-mappen",
"TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@@ -121,8 +121,8 @@
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
"TaskOptimizeDatabase": "Optimér database",
- "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
- "TaskKeyframeExtractor": "Nøglebillede udtræk",
+ "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
+ "TaskKeyframeExtractor": "Udtræk af nøglebillede",
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index e1c3e9de1..f1dbf3c89 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
"TaskKeyframeExtractor": "Keyframe Extraktor",
"External": "Extern",
- "HearingImpaired": "Hörgeschädigt"
+ "HearingImpaired": "Hörgeschädigt",
+ "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
+ "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 243688388..32bf89310 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskKeyframeExtractor": "Keyframe Extractor",
"External": "External",
- "HearingImpaired": "Hearing Impaired"
+ "HearingImpaired": "Hearing Impaired",
+ "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+ "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 15088384c..496ecabd3 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -112,6 +112,8 @@
"TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
"TaskRefreshPeople": "Refresh People",
"TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
+ "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+ "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
"TaskUpdatePlugins": "Update Plugins",
"TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
"TaskCleanTranscode": "Clean Transcode Directory",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index f5636a0af..fe10be308 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
- "AuthenticationSucceededWithUserName": "{0} identificado correctamente",
+ "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
- "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
+ "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"External": "Externo",
- "HearingImpaired": "Discapacidad Auditiva"
+ "HearingImpaired": "Discapacidad Auditiva",
+ "TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
+ "TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 01b3e95fc..88a4a358e 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -123,5 +123,6 @@
"HearingImpaired": "Bingi",
"TaskKeyframeExtractor": "Tagabunot ng Keyframe",
"TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
- "External": "External"
+ "External": "External",
+ "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
new file mode 100644
index 000000000..40aa5f71a
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -0,0 +1,18 @@
+{
+ "Artists": "Listafólk",
+ "Collections": "Søvn",
+ "Default": "Sjálvgildi",
+ "DeviceOfflineWithName": "{0} hevur slitið sambandið",
+ "External": "Ytri",
+ "Genres": "Greinar",
+ "Albums": "Album",
+ "AppDeviceValues": "App: {0}, Eind: {1}",
+ "Application": "Nýtsluskipan",
+ "Books": "Bøkur",
+ "Channels": "Rásir",
+ "ChapterNameValue": "Kapittul {0}",
+ "DeviceOnlineWithName": "{0} er sambundið",
+ "Favorites": "Yndis",
+ "Folders": "Mappur",
+ "Forced": "Kravt"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 4877bcd7a..03002476c 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une photo a été téléversée depuis {0}",
+ "CameraImageUploadedFrom": "Une photo a été téléchargée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
@@ -16,14 +16,14 @@
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album",
- "HeaderContinueWatching": "Reprendre le visionnage",
+ "HeaderContinueWatching": "Continuer de regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
"HeaderFavoriteEpisodes": "Épisodes favoris",
"HeaderFavoriteShows": "Séries favorites",
"HeaderFavoriteSongs": "Chansons préférées",
"HeaderLiveTV": "TV en direct",
- "HeaderNextUp": "À suivre",
+ "HeaderNextUp": "Prochain à venir",
"HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
"Inherit": "Hériter",
@@ -71,7 +71,7 @@
"ScheduledTaskStartedWithName": "{0} a démarré",
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Séries",
- "Songs": "Titres",
+ "Songs": "Chansons",
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
"SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
@@ -105,8 +105,8 @@
"TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
- "TaskRefreshLibrary": "Scanner la médiathèque",
+ "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
+ "TaskRefreshLibrary": "Analyser la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
@@ -122,7 +122,9 @@
"TaskOptimizeDatabaseDescription": "Réduit les espaces vides ou inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la médiathèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
"TaskOptimizeDatabase": "Optimiser la base de données",
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
- "TaskKeyframeExtractor": "Extracteur d'image clé",
+ "TaskKeyframeExtractor": "Extracteur d'images clés",
"External": "Externe",
- "HearingImpaired": "Malentendants"
+ "HearingImpaired": "Malentendants",
+ "TaskRefreshTrickplayImages": "Générer des images Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 68e9fe833..26eab392e 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.",
"TaskKeyframeExtractor": "מחלץ תמונות מפתח",
"External": "חיצוני",
- "HearingImpaired": "לקוי שמיעה"
+ "HearingImpaired": "לקוי שמיעה",
+ "TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
+ "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 5a4a02d80..ba3d5872a 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractor": "Kulcsképkockák kibontása",
"TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
"External": "Külső",
- "HearingImpaired": "Hallássérült"
+ "HearingImpaired": "Hallássérült",
+ "TaskRefreshTrickplayImages": "Trickplay képek generálása",
+ "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz."
}
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index a40f49506..0f1f0b3d2 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -13,8 +13,8 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa",
- "HeaderAlbumArtists": "Höfundur plötu",
- "Genres": "Tegundir",
+ "HeaderAlbumArtists": "Listamaður á umslagi",
+ "Genres": "Stefnur",
"Folders": "Möppur",
"Favorites": "Uppáhalds",
"FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
@@ -22,32 +22,32 @@
"DeviceOfflineWithName": "{0} hefur aftengst",
"Collections": "Söfn",
"ChapterNameValue": "Kafli {0}",
- "Channels": "Stöðvar",
- "CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}",
+ "Channels": "Rásir",
+ "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
"Books": "Bækur",
- "AuthenticationSucceededWithUserName": "{0} auðkenning tókst",
- "Artists": "Listamaður",
+ "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
+ "Artists": "Listamenn",
"Application": "Forrit",
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
"Albums": "Plötur",
- "Plugin": "Viðbót",
- "Photos": "Myndir",
- "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
- "NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
+ "Plugin": "Viðbótarvirkni",
+ "Photos": "Ljósmyndir",
+ "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
+ "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
"NotificationOptionUserLockedOut": "Notandi læstur úti",
- "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg",
- "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
- "NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
- "NotificationOptionPluginInstalled": "Viðbót sett upp",
+ "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg",
+ "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið",
+ "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð",
+ "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp",
"NotificationOptionPluginError": "Bilun í viðbót",
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
- "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
+ "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp",
"NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
"NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
"NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
"NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
- "NameSeasonUnknown": "Sería óþekkt",
- "NameSeasonNumber": "Sería {0}",
+ "NameSeasonUnknown": "Þáttaröð óþekkt",
+ "NameSeasonNumber": "Þáttaröð {0}",
"MixedContent": "Blandað efni",
"MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
"MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
@@ -57,24 +57,24 @@
"User": "Notandi",
"System": "Kerfi",
"NotificationOptionNewLibraryContent": "Nýju efni bætt við",
- "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.",
+ "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.",
"NameInstallFailed": "{0} uppsetning mistókst",
"MusicVideos": "Tónlistarmyndbönd",
"Music": "Tónlist",
"Movies": "Kvikmyndir",
"UserDeletedWithName": "Notanda {0} hefur verið eytt",
"UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
- "TvShows": "Þættir",
+ "TvShows": "Sjónvarpsþættir",
"Sync": "Samstilla",
"Songs": "Lög",
- "ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa",
+ "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
"ScheduledTaskStartedWithName": "{0} hafin",
"ScheduledTaskFailedWithName": "{0} mistókst",
"PluginUpdatedWithName": "{0} var uppfært",
"PluginUninstalledWithName": "{0} var fjarlægt",
"PluginInstalledWithName": "{0} var sett upp",
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
- "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.",
+ "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.",
"VersionNumber": "Útgáfa {0}",
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
@@ -83,14 +83,14 @@
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
- "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur",
- "UserDownloadingItemWithValues": "{0} Hleður niður {1}",
+ "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
+ "UserDownloadingItemWithValues": "{0} hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
- "ProviderValue": "Veitandi: {0}",
+ "ProviderValue": "Efnisveita: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
- "ValueSpecialEpisodeName": "Sérstakt - {0}",
- "Shows": "Sýningar",
- "Playlists": "Spilunarlisti",
+ "ValueSpecialEpisodeName": "Sérstaktur - {0}",
+ "Shows": "Þættir",
+ "Playlists": "Efnisskrár",
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
"TaskRefreshChannels": "Endurhlaða Rásir",
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
@@ -116,5 +116,12 @@
"TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
"TaskCleanLogs": "Hreinsa færslu skrá",
"TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
- "HearingImpaired": "Heyrnarskertur"
+ "HearingImpaired": "Heyrnarskertur",
+ "TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.",
+ "TaskKeyframeExtractor": "Lykilrammaplokkari",
+ "TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.",
+ "TaskRefreshChapterImages": "Plokka kafla-myndir",
+ "TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
+ "Forced": "Þvingað",
+ "External": "Útvær"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 7b059c68e..ab6988006 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -4,19 +4,19 @@
"Application": "アプリケーション",
"Artists": "アーティスト",
"AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
- "Books": "ブックス",
+ "Books": "ブック",
"CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
"Channels": "チャンネル",
"ChapterNameValue": "チャプター {0}",
"Collections": "コレクション",
- "DeviceOfflineWithName": "{0} が切断されました",
- "DeviceOnlineWithName": "{0} が接続されました",
- "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} によって失敗しました",
+ "DeviceOfflineWithName": "{0} が切断しました",
+ "DeviceOnlineWithName": "{0} が接続しました",
+ "FailedLoginAttemptWithUserName": "{0} からのログインに失敗しました",
"Favorites": "お気に入り",
"Folders": "フォルダー",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
- "HeaderContinueWatching": "続けて見る",
+ "HeaderContinueWatching": "再生を続ける",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
"HeaderFavoriteEpisodes": "お気に入りのエピソード",
@@ -27,22 +27,22 @@
"HeaderRecordingGroups": "レコーディンググループ",
"HomeVideos": "ホームビデオ",
"Inherit": "継承",
- "ItemAddedWithName": "{0} をライブラリに追加しました",
- "ItemRemovedWithName": "{0} をライブラリから削除しました",
+ "ItemAddedWithName": "{0} をライブラリーに追加しました",
+ "ItemRemovedWithName": "{0} をライブラリーから削除しました",
"LabelIpAddressValue": "IPアドレス: {0}",
- "LabelRunningTimeValue": "稼働時間: {0}",
+ "LabelRunningTimeValue": "時間: {0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin Server が更新されました",
- "MessageApplicationUpdatedTo": "Jellyfin Server が {0}に更新されました",
- "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました",
- "MessageServerConfigurationUpdated": "サーバー設定が更新されました",
+ "MessageApplicationUpdated": "Jellyfin Server を更新しました",
+ "MessageApplicationUpdatedTo": "Jellyfin Server を {0}に更新しました",
+ "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} を更新しました",
+ "MessageServerConfigurationUpdated": "サーバー設定を更新しました",
"MixedContent": "ミックスコンテンツ",
"Movies": "映画",
"Music": "音楽",
"MusicVideos": "ミュージックビデオ",
"NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}",
- "NameSeasonUnknown": "不明なシーズン",
+ "NameSeasonUnknown": "シーズン不明",
"NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。",
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
@@ -88,18 +88,18 @@
"UserPolicyUpdatedWithName": "ユーザーポリシーが{0}に更新されました",
"UserStartedPlayingItemWithValues": "{0} は {2}で{1} を再生しています",
"UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました",
- "ValueHasBeenAddedToLibrary": "{0}はあなたのメディアライブラリに追加されました",
+ "ValueHasBeenAddedToLibrary": "{0} をメディアライブラリーに追加しました",
"ValueSpecialEpisodeName": "スペシャル - {0}",
"VersionNumber": "バージョン {0}",
"TaskCleanLogsDescription": "{0} 日以上前のログを消去します。",
"TaskCleanLogs": "ログの掃除",
- "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータを更新します。",
- "TaskRefreshLibrary": "メディアライブラリのスキャン",
+ "TaskRefreshLibraryDescription": "メディアライブラリーをスキャンして、新しいファイルを探し、メタデータを更新します。",
+ "TaskRefreshLibrary": "メディアライブラリーをスキャン",
"TaskCleanCacheDescription": "不要なキャッシュを消去します。",
"TaskCleanCache": "キャッシュを消去",
"TasksChannelsCategory": "ネットチャンネル",
"TasksApplicationCategory": "アプリケーション",
- "TasksLibraryCategory": "ライブラリ",
+ "TasksLibraryCategory": "ライブラリー",
"TasksMaintenanceCategory": "メンテナンス",
"TaskRefreshChannelsDescription": "ネットチャンネルの情報を更新する。",
"TaskRefreshChannels": "チャンネルの更新",
@@ -107,7 +107,7 @@
"TaskCleanTranscode": "トランスコードディレクトリの削除",
"TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。",
"TaskUpdatePlugins": "プラグインの更新",
- "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータを更新します。",
+ "TaskRefreshPeopleDescription": "メディアライブラリー内の俳優や監督のメタデータを更新します。",
"TaskRefreshPeople": "俳優や監督のデータの更新",
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索する。",
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
@@ -118,10 +118,12 @@
"Undefined": "未定義",
"Forced": "強制",
"Default": "デフォルト",
- "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリのスキャン後でこのタスクを実行するとパフォーマンスが向上する可能性があります。",
+ "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリーのスキャンやその他のデータベースの更新を伴う変更の後でこのタスクを実行すると、パフォーマンスが向上します。",
"TaskOptimizeDatabase": "データベースの最適化",
"TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。",
"TaskKeyframeExtractor": "キーフレーム抽出",
"External": "外部",
- "HearingImpaired": "聴覚障害の方"
+ "HearingImpaired": "聴覚障害の方",
+ "TaskRefreshTrickplayImages": "トリックプレー画像を生成",
+ "TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index c5a93cb96..e050196bc 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -123,5 +123,8 @@
"TaskOptimizeDatabase": "Derekqordy oñtailandyru",
"TaskKeyframeExtractorDescription": "Naqtyraq HLS oynatu tızımderın jasau üşın beinefaildardan negızgı kadrlardy şyğarady. Būl tapsyrma ūzaq uaqytqa sozyluy mümkın.",
"TaskKeyframeExtractor": "Negızgı kadrlardy şyğaru",
- "External": "Syrtqy"
+ "External": "Syrtqy",
+ "TaskRefreshTrickplayImagesDescription": "Іске қосылған кітапханалардағы бейнелер үшін Trickplay алдын ала түрінде көрсетілімді жасайды.",
+ "TaskRefreshTrickplayImages": "Trickplay үшін суреттерді жасау",
+ "HearingImpaired": "Есту қабілеті нашарға"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json
index 3c8c38ed4..5e2b3756b 100644
--- a/Emby.Server.Implementations/Localization/Core/kn.json
+++ b/Emby.Server.Implementations/Localization/Core/kn.json
@@ -3,5 +3,125 @@
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
- "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
+ "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
+ "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
+ "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
+ "TasksLibraryCategory": "ಸಮೊಹ",
+ "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
+ "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
+ "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
+ "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
+ "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+ "Albums": "ಸಂಪುಟ",
+ "Application": "ಅಪ್ಲಿಕೇಶನ್",
+ "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
+ "Artists": "ಕಲಾವಿದರು",
+ "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
+ "Books": "ಪುಸ್ತಕಗಳು",
+ "ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
+ "Collections": "ಸಂಗ್ರಹಣೆಗಳು",
+ "Default": "ಪೂರ್ವನಿಯೋಜಿತ",
+ "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+ "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
+ "External": "ಹೊರಗಿನ",
+ "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
+ "Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
+ "Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
+ "Forced": "ಬಲವಂತವಾಗಿ",
+ "Genres": "ಪ್ರಕಾರಗಳು",
+ "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
+ "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
+ "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
+ "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
+ "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
+ "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
+ "HeaderNextUp": "ಮುಂದೆ",
+ "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
+ "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+ "Channels": "ಮೂಲಗಳು",
+ "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
+ "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
+ "HearingImpaired": "ಮೂಗ",
+ "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
+ "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+ "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.",
+ "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+ "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+ "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+ "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
+ "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+ "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+ "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
+ "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
+ "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
+ "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
+ "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
+ "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ",
+ "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
+ "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+ "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
+ "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
+ "VersionNumber": "ಆವೃತ್ತಿ {0}",
+ "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
+ "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
+ "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
+ "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
+ "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
+ "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
+ "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
+ "Shows": "ಧಾರವಾಹಿಗಳು",
+ "Songs": "ಹಾಡುಗಳು",
+ "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
+ "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
+ "UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
+ "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
+ "Sync": "ಹೊಂದಿಕೆ",
+ "System": "ವ್ಯವಸ್ಥೆ",
+ "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
+ "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
+ "User": "ಬಳಕೆದಾರ",
+ "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
+ "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
+ "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
+ "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
+ "LabelRunningTimeValue": "ಅವಧಿ: {0}",
+ "Latest": "ಹೊಸದಾದ",
+ "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
+ "Movies": "ಚಲನಚಿತ್ರಗಳು",
+ "Music": "ಸಂಗೀತ",
+ "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
+ "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
+ "NameSeasonNumber": "ಸೀಸನ್ {0}",
+ "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
+ "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
+ "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+ "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
+ "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
+ "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+ "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+ "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
+ "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
+ "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+ "Photos": "ಚಿತ್ರಗಳು",
+ "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
+ "Plugin": "ಪ್ಲಗಿನ್",
+ "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "ProviderValue": "ಒದಗಿಸುವವರು: {0}",
+ "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
+ "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+ "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
+ "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
+ "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
+ "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+ "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index f7b24412a..82a071309 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -1,7 +1,7 @@
{
"ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
- "HeaderRecordingGroups": "Ierakstu Grupas",
+ "HeaderRecordingGroups": "Ierakstu grupas",
"UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
@@ -14,13 +14,13 @@
"Photos": "Attēli",
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
"LabelRunningTimeValue": "Garums: {0}",
- "Inherit": "Mantot",
+ "Inherit": "Pārmantot",
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}",
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
- "UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
+ "UserPasswordChangedWithName": "Lietotāja {0} parole tika nomainīta",
"UserOnlineFromDevice": "{0} ir tiešsaistē no {1}",
"UserOfflineFromDevice": "{0} ir atvienojies no {1}",
"UserLockedOutWithName": "Lietotājs {0} ir ticis bloķēts",
@@ -28,23 +28,23 @@
"UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
"UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
"User": "Lietotājs",
- "TvShows": "TV Raidījumi",
+ "TvShows": "TV raidījumi",
"Sync": "Sinhronizācija",
"System": "Sistēma",
"StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
"Songs": "Dziesmas",
- "Shows": "Raidījumi",
+ "Shows": "Šovi",
"PluginUpdatedWithName": "{0} tika atjaunots",
"PluginUninstalledWithName": "{0} tika noņemts",
"PluginInstalledWithName": "{0} tika uzstādīts",
"Plugin": "Paplašinājums",
- "Playlists": "Atskaņošanas Saraksti",
+ "Playlists": "Atskaņošanas saraksti",
"MixedContent": "Jaukts saturs",
- "HomeVideos": "Mājas Video",
+ "HomeVideos": "Mājas video",
"HeaderNextUp": "Nākamais",
- "ChapterNameValue": "Nodaļa {0}",
+ "ChapterNameValue": "{0}. nodaļa",
"Application": "Lietotne",
- "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
+ "NotificationOptionServerRestartRequired": "Nepieciešams servera restarts",
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
"NotificationOptionPluginUninstalled": "Paplašinājums noņemts",
"NotificationOptionPluginInstalled": "Paplašinājums uzstādīts",
@@ -56,14 +56,14 @@
"NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
"NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
"NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
- "NameSeasonUnknown": "Nezināma Sezona",
- "NameSeasonNumber": "Sezona {0}",
+ "NameSeasonUnknown": "Nezināma sezona",
+ "NameSeasonNumber": "{0}. sezona",
"NameInstallFailed": "{0} instalācija neizdevās",
"MusicVideos": "Mūzikas video",
"Music": "Mūzika",
"Movies": "Filmas",
"MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
- "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota",
"MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
"MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
"Latest": "Jaunākais",
@@ -71,57 +71,57 @@
"ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV",
- "HeaderContinueWatching": "Turpināt Skatīšanos",
- "HeaderAlbumArtists": "Albumu Izpildītāji",
+ "HeaderContinueWatching": "Turpini skatīties",
+ "HeaderAlbumArtists": "Albumu izpildītāji",
"Genres": "Žanri",
"Folders": "Mapes",
- "Favorites": "Favorīti",
- "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}",
- "DeviceOnlineWithName": "{0} ir pievienojies",
- "DeviceOfflineWithName": "{0} ir atvienojies",
+ "Favorites": "Izlase",
+ "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
+ "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
+ "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas",
"Channels": "Kanāli",
- "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
+ "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
"Books": "Grāmatas",
"Artists": "Izpildītāji",
"Albums": "Albumi",
"ProviderValue": "Provider: {0}",
- "HeaderFavoriteSongs": "Dziesmu Favorīti",
- "HeaderFavoriteShows": "Raidījumu Favorīti",
- "HeaderFavoriteEpisodes": "Episožu Favorīti",
- "HeaderFavoriteArtists": "Izpildītāju Favorīti",
- "HeaderFavoriteAlbums": "Albumu Favorīti",
- "TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.",
- "TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
+ "HeaderFavoriteSongs": "Dziesmu izlase",
+ "HeaderFavoriteShows": "Raidījumu izlase",
+ "HeaderFavoriteEpisodes": "Sēriju izlase",
+ "HeaderFavoriteArtists": "Izpildītāju izlase",
+ "HeaderFavoriteAlbums": "Albumu izlase",
+ "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
+ "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka",
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
- "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
+ "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
- "TaskRefreshChannels": "Atjaunot Kanālus",
- "TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.",
- "TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
+ "TaskRefreshChannels": "Atjaunot kanālus",
+ "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
+ "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi",
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
- "TaskUpdatePlugins": "Atjaunot Paplašinājumus",
+ "TaskUpdatePlugins": "Atjaunot paplašinājumus",
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
- "TaskRefreshPeople": "Atjaunot Cilvēkus",
- "TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
- "TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
+ "TaskRefreshPeople": "Atjaunot cilvēkus",
+ "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
+ "TaskCleanLogs": "Iztīrīt logdatņu mapi",
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
- "TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
+ "TaskRefreshLibrary": "Skenēt multivides bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
- "TaskCleanCache": "Iztīrīt Kešošanas Mapi",
- "TasksChannelsCategory": "Interneta Kanāli",
+ "TaskCleanCache": "Iztīrīt kešatmiņas mapi",
+ "TasksChannelsCategory": "Interneta kanāli",
"TasksMaintenanceCategory": "Apkope",
- "Forced": "Piespiests",
+ "Forced": "Piespiedu",
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
- "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
+ "TaskCleanActivityLog": "Notīrīt darbību žurnālu",
"Undefined": "Nenoteikts",
"Default": "Noklusējuma",
- "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
+ "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
"TaskOptimizeDatabase": "Optimizēt datubāzi",
"External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem",
- "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
+ "TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index 0620fbcdb..0b50fa529 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -121,5 +121,7 @@
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
"HearingImpaired": "കേൾവി തകരാറുകൾ",
- "External": "പുറമേയുള്ള"
+ "External": "പുറമേയുള്ള",
+ "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
+ "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index b2293e4b6..a07222975 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -1,5 +1,5 @@
{
- "Albums": "Album-album",
+ "Albums": "Album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
"Application": "Aplikasi",
"Artists": "Artis-artis",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 4eb00d289..be397f1b8 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -1,9 +1,9 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
- "Application": "Toepassing",
+ "Application": "Applicatie",
"Artists": "Artiesten",
- "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
+ "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
"Books": "Boeken",
"CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen",
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
"TaskKeyframeExtractor": "Keyframe-uitpakker",
"External": "Extern",
- "HearingImpaired": "Slechthorend"
+ "HearingImpaired": "Slechthorend",
+ "TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
+ "TaskRefreshTrickplayImagesDescription": "Genereert trickplay-afbeeldingen voor video's in bibliotheken waarvoor dit is ingeschakeld."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index d4c15ac87..bd572b744 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -124,5 +124,7 @@
"External": "Zewnętrzny",
"TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
- "HearingImpaired": "Niedosłyszący"
+ "HearingImpaired": "Niedosłyszący",
+ "TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index 87800a2fe..26dc5ce82 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -24,5 +24,13 @@
"TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
"HeaderAlbumArtists": "Buccaneers o' the musical arts",
"HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
- "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas"
+ "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas",
+ "Channels": "Channels",
+ "Forced": "Pressed",
+ "External": "Outboard",
+ "HeaderFavoriteEpisodes": "Treasured Tales",
+ "HeaderFavoriteShows": "Treasured Tales",
+ "ChapterNameValue": "Piece {0}",
+ "HeaderFavoriteSongs": "Treasured Chimes",
+ "HeaderNextUp": "Incoming"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 2281e80c8..103393a1e 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -92,7 +92,7 @@
"Application": "Aplicação",
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
- "TasksApplicationCategory": "Aplicativo",
+ "TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskRefreshChannels": "Atualizar Canais",
@@ -123,5 +123,7 @@
"External": "Externo",
"HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
- "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo."
+ "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
+ "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
+ "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 421513341..26d678a0c 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -31,13 +31,13 @@
"ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}",
- "Latest": "Новое",
+ "Latest": "Последние добавленные",
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
"MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
"MixedContent": "Смешанное содержание",
- "Movies": "Кино",
+ "Movies": "Фильмы",
"Music": "Музыка",
"MusicVideos": "Муз. видео",
"NameInstallFailed": "Установка {0} неудачна",
@@ -77,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
"Sync": "Синхронизация",
"System": "Система",
- "TvShows": "ТВ",
+ "TvShows": "Телесериалы",
"User": "Пользователь",
"UserCreatedWithName": "Пользователь {0} был создан",
"UserDeletedWithName": "Пользователь {0} был удалён",
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.",
"TaskKeyframeExtractor": "Извлечение ключевых кадров",
"External": "Внешние",
- "HearingImpaired": "Для слабослышащих"
+ "HearingImpaired": "Для слабослышащих",
+ "TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена."
}
diff --git a/Emby.Server.Implementations/Localization/Core/si.json b/Emby.Server.Implementations/Localization/Core/si.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/si.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 858cc40dd..43594a42e 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
"External": "Externé",
- "HearingImpaired": "Sluchovo Postihnutý"
+ "HearingImpaired": "Sluchovo postihnutí",
+ "TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 4c23f71ef..1944e072c 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -11,7 +11,7 @@
"Collections": "Zbirke",
"DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan",
- "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
+ "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Priljubljeno",
"Folders": "Mape",
"Genres": "Zvrsti",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 785e6b226..97062deec 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.",
"TaskKeyframeExtractor": "Extraktor för nyckelbildrutor",
"External": "Extern",
- "HearingImpaired": "Hörselskadad"
+ "HearingImpaired": "Hörselskadad",
+ "TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
+ "TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 770624a8d..646d7d7a5 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -102,7 +102,7 @@
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இல் இருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
- "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+ "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்கும்படி கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்கி நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
"TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்",
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
"TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்",
"External": "வெளி",
- "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்"
+ "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
+ "TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
+ "TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 9a140f871..3ce928859 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -3,19 +3,19 @@
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
"Application": "Uygulama",
"Artists": "Sanatçılar",
- "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı",
+ "AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
"Books": "Kitaplar",
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar",
- "ChapterNameValue": "Bölüm {0}",
+ "ChapterNameValue": "{0}. Bölüm",
"Collections": "Koleksiyonlar",
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı",
- "FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu",
+ "FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
"Favorites": "Favoriler",
"Folders": "Klasörler",
"Genres": "Türler",
- "HeaderAlbumArtists": "Albüm Sanatçıları",
+ "HeaderAlbumArtists": "Albüm sanatçıları",
"HeaderContinueWatching": "İzlemeye Devam Et",
"HeaderFavoriteAlbums": "Favori Albümler",
"HeaderFavoriteArtists": "Favori Sanatçılar",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Canlı TV",
"HeaderNextUp": "Gelecek Hafta",
"HeaderRecordingGroups": "Kayıt Grupları",
- "HomeVideos": "Ana sayfa videoları",
+ "HomeVideos": "Ana Sayfa Videoları",
"Inherit": "Devral",
"ItemAddedWithName": "{0} kütüphaneye eklendi",
"ItemRemovedWithName": "{0} kütüphaneden silindi",
@@ -34,14 +34,14 @@
"Latest": "En son",
"MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
"MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi",
- "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi",
- "MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi",
+ "MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi",
"MixedContent": "Karışık içerik",
"Movies": "Filmler",
"Music": "Müzik",
- "MusicVideos": "Müzik videoları",
+ "MusicVideos": "Müzik Videoları",
"NameInstallFailed": "{0} kurulumu başarısız",
- "NameSeasonNumber": "Sezon {0}",
+ "NameSeasonNumber": "{0}. Sezon",
"NameSeasonUnknown": "Bilinmeyen Sezon",
"NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
"NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
@@ -55,9 +55,9 @@
"NotificationOptionPluginInstalled": "Eklenti yüklendi",
"NotificationOptionPluginUninstalled": "Eklenti kaldırıldı",
"NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi",
- "NotificationOptionServerRestartRequired": "Sunucu yeniden başlatma gerekli",
+ "NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılması gerekiyor",
"NotificationOptionTaskFailed": "Zamanlanmış görev hatası",
- "NotificationOptionUserLockedOut": "Kullanıcı kitlendi",
+ "NotificationOptionUserLockedOut": "Kullanıcı kilitlendi",
"NotificationOptionVideoPlayback": "Video oynatma başladı",
"NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu",
"Photos": "Fotoğraflar",
@@ -74,36 +74,36 @@
"Songs": "Şarkılar",
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi",
+ "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi",
"Sync": "Eşzamanlama",
"System": "Sistem",
"TvShows": "Diziler",
"User": "Kullanıcı",
"UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
- "UserDeletedWithName": "Kullanıcı {0} silindi",
- "UserDownloadingItemWithValues": "{0} indiriliyor {1}",
- "UserLockedOutWithName": "Kullanıcı {0} kitlendi",
- "UserOfflineFromDevice": "{0}, {1} ile bağlantısı kesildi",
- "UserOnlineFromDevice": "{0}, {1} çevrimiçi",
- "UserPasswordChangedWithName": "{0} kullanıcısı için şifre değiştirildi",
- "UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi",
+ "UserDeletedWithName": "{0} kullanıcısı silindi",
+ "UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor",
+ "UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi",
+ "UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi",
+ "UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi",
+ "UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi",
+ "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
"ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
"ValueSpecialEpisodeName": "Özel - {0}",
"VersionNumber": "Sürüm {0}",
- "TaskCleanCache": "Geçici dosya klasörünü temizle",
- "TasksChannelsCategory": "İnternet kanalları",
+ "TaskCleanCache": "Geçici Dosya Klasörünü Temizle",
+ "TasksChannelsCategory": "İnternet Kanalları",
"TasksApplicationCategory": "Uygulama",
"TasksLibraryCategory": "Kütüphane",
"TasksMaintenanceCategory": "Bakım",
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
- "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
+ "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
"TaskRefreshChannels": "Kanalları Yenile",
- "TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.",
- "TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
+ "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
+ "TaskCleanTranscode": "Kod Dönüştürme Dizinini Temizle",
"TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
"TaskUpdatePlugins": "Eklentileri Güncelle",
"TaskRefreshPeople": "Kullanıcıları Yenile",
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index ff77fb8c5..bd5398f08 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.",
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
"External": "Зовнішній",
- "HearingImpaired": "З порушеннями слуху"
+ "HearingImpaired": "З порушеннями слуху",
+ "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
+ "TaskRefreshTrickplayImages": "Створення Trickplay-зображень"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 03265d3fb..b88d4eeaf 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -121,8 +121,10 @@
"Default": "默认",
"TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。",
"TaskOptimizeDatabase": "优化数据库",
- "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
+ "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的 HLS 播放列表。这项任务可能需要很长时间。",
"TaskKeyframeExtractor": "关键帧提取器",
"External": "外部",
- "HearingImpaired": "听力障碍"
+ "HearingImpaired": "听力障碍",
+ "TaskRefreshTrickplayImages": "生成时间轴缩略图",
+ "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 36f4df93d..d57a2811d 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。",
"TaskKeyframeExtractor": "關鍵幀提取器",
"External": "外部",
- "HearingImpaired": "聽力障礙"
+ "HearingImpaired": "聽力障礙",
+ "TaskRefreshTrickplayImages": "生成快轉縮圖",
+ "TaskRefreshTrickplayImagesDescription": "為啟用此設定的媒體庫生成快轉縮圖。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json
index b5f4b920f..aa056d449 100644
--- a/Emby.Server.Implementations/Localization/Core/zu.json
+++ b/Emby.Server.Implementations/Localization/Core/zu.json
@@ -25,5 +25,14 @@
"Channels": "Amashaneli",
"Books": "Izincwadi",
"Artists": "Abadlali",
- "Albums": "Ama-albhamu"
+ "Albums": "Ama-albhamu",
+ "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
+ "HeaderFavoriteArtists": "Abasethi Abathandekayo",
+ "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
+ "HeaderFavoriteShows": "Izisho Ezithandekayo",
+ "External": "Kwezifungo",
+ "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
+ "HeaderContinueWatching": "Buyela Ukubona",
+ "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
+ "HeaderAlbumArtists": "Abasethi wenkulumo"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 96f435399..16776b6bd 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization
string countryCode = resource.Substring(RatingsPath.Length, 2);
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
- await using var stream = _assembly.GetManifestResourceStream(resource);
- using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ var stream = _assembly.GetManifestResourceStream(resource);
+ await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
{
- if (string.IsNullOrWhiteSpace(line))
+ using var reader = new StreamReader(stream!);
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- continue;
- }
-
- string[] parts = line.Split(',');
- if (parts.Length == 2
- && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- var name = parts[0];
- dict.Add(name, new ParentalRating(name, value));
- }
- else
- {
- _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ string[] parts = line.Split(',');
+ if (parts.Length == 2
+ && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ {
+ var name = parts[0];
+ dict.Add(name, new ParentalRating(name, value));
+ }
+ else
+ {
+ _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
+ }
}
}
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv
index 0bc1d3f7d..619e948d8 100644
--- a/Emby.Server.Implementations/Localization/Ratings/es.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/es.csv
@@ -3,6 +3,7 @@ A/fig,0
A/i,0
A/fig/i,0
APTA,0
+ERI,0
TP,0
0+,0
6+,6
diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv
index 774a70589..139ea376b 100644
--- a/Emby.Server.Implementations/Localization/Ratings/fr.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/fr.csv
@@ -1,5 +1,6 @@
Public Averti,0
Tous Publics,0
+TP,0
U,0
0+,0
6+,6
diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv
new file mode 100644
index 000000000..dbafd8efa
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/sk.csv
@@ -0,0 +1,6 @@
+NR,0
+U,0
+7,7
+12,12
+15,15
+18,18
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 7732e32d0..896f47923 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
{
var deadImages = images
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
- .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var image in deadImages)
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 51e92953d..2bcd5eab2 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,12 +1,15 @@
-#pragma warning disable CS1591
-
using System;
+using System.Linq;
using System.Net;
+using System.Net.NetworkInformation;
using System.Net.Sockets;
using MediaBrowser.Model.Net;
namespace Emby.Server.Implementations.Net
{
+ /// <summary>
+ /// Factory class to create different kinds of sockets.
+ /// </summary>
public class SocketFactory : ISocketFactory
{
/// <inheritdoc />
@@ -29,7 +32,7 @@ namespace Emby.Server.Implementations.Net
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
@@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
/// <inheritdoc />
public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
{
- ArgumentNullException.ThrowIfNull(bindInterface.Address);
+ var interfaceAddress = bindInterface.Address;
+ ArgumentNullException.ThrowIfNull(interfaceAddress);
if (localPort < 0)
{
@@ -49,13 +53,13 @@ namespace Emby.Server.Implementations.Net
try
{
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- socket.Bind(new IPEndPoint(bindInterface.Address, localPort));
+ socket.Bind(new IPEndPoint(interfaceAddress, localPort));
return socket;
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
@@ -82,22 +86,31 @@ namespace Emby.Server.Implementations.Net
try
{
- var interfaceIndex = bindInterface.Index;
- var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex);
-
socket.MulticastLoopback = false;
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
- socket.Bind(new IPEndPoint(multicastAddress, localPort));
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
+ socket.Bind(new IPEndPoint(multicastAddress, localPort));
+ }
+ else
+ {
+ // Only create socket if interface supports multicast
+ var interfaceIndex = bindInterface.Index;
+ var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
+
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
+ socket.Bind(new IPEndPoint(bindIPAddress, localPort));
+ }
return socket;
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 702f8d45b..649c49924 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
// this is probably best done as a metadata provider
// saving a file over itself will require some work to prevent this from happening when not needed
var playlistPath = item.Path;
- var extension = Path.GetExtension(playlistPath);
+ var extension = Path.GetExtension(playlistPath.AsSpan());
- if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
+ if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
{
var playlist = new WplPlaylist();
foreach (var child in item.GetLinkedChildren())
@@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new WplContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
{
var playlist = new ZplPlaylist();
foreach (var child in item.GetLinkedChildren())
@@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new ZplContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
{
var playlist = new M3uPlaylist
{
@@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
var playlist = new M3uPlaylist();
playlist.IsExtended = true;
@@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
{
var playlist = new PlsPlaylist();
foreach (var child in item.GetLinkedChildren())
@@ -518,6 +514,11 @@ namespace Emby.Server.Implementations.Playlists
return relativePath;
}
+ public Folder GetPlaylistsFolder()
+ {
+ return GetPlaylistsFolder(Guid.Empty);
+ }
+
public Folder GetPlaylistsFolder(Guid userId)
{
const string TypeName = "PlaylistsFolder";
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 48584ae0c..20793ee39 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -11,7 +10,6 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
-using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
@@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins
/// <summary>
/// Defines the <see cref="PluginManager" />.
/// </summary>
- public class PluginManager : IPluginManager
+ public sealed class PluginManager : IPluginManager, IDisposable
{
private const string MetafileName = "meta.json";
@@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins
}
}
- /// <inheritdoc />
- public void UnloadAssemblies()
- {
- foreach (var assemblyLoadContext in _assemblyLoadContexts)
- {
- assemblyLoadContext.Unload();
- }
- }
-
/// <summary>
/// Creates all the plugin instances.
/// </summary>
@@ -397,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
- await using var fileStream = AsyncFile.OpenWrite(imagePath);
-
+ var fileStream = AsyncFile.OpenWrite(imagePath);
+ Stream? downloadStream = null;
try
{
- await using var downloadStream = await HttpClientFactory
+ downloadStream = await HttpClientFactory
.CreateClient(NamedClient.Default)
.GetStreamAsync(url)
.ConfigureAwait(false);
@@ -413,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
imagePath = string.Empty;
}
+ finally
+ {
+ await fileStream.DisposeAsync().ConfigureAwait(false);
+ if (downloadStream is not null)
+ {
+ await downloadStream.DisposeAsync().ConfigureAwait(false);
+ }
+ }
}
var manifest = new PluginManifest
@@ -432,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins
ImagePath = imagePath
};
- if (!await ReconcileManifest(manifest, path))
+ if (!await ReconcileManifest(manifest, path).ConfigureAwait(false))
{
// An error occurred during reconciliation and saving could be undesirable.
return false;
@@ -441,6 +438,15 @@ namespace Emby.Server.Implementations.Plugins
return SaveManifest(manifest, path);
}
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ foreach (var assemblyLoadContext in _assemblyLoadContexts)
+ {
+ assemblyLoadContext.Unload();
+ }
+ }
+
/// <summary>
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
/// If no file is found, no reconciliation occurs.
@@ -460,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins
}
using var metaStream = File.OpenRead(metafile);
- var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions);
+ var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions).ConfigureAwait(false);
localManifest ??= new PluginManifest();
if (!Equals(localManifest.Id, manifest.Id))
@@ -677,7 +683,7 @@ namespace Emby.Server.Implementations.Plugins
}
catch (JsonException ex)
{
- _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data!));
+ _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data));
}
if (manifest is not null)
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 6ad6c4cbd..5d15c3a21 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
try
{
- previouslyFailedImages = File.ReadAllText(failHistoryPath)
+ previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
.Split('|', StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
@@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
string text = string.Join('|', previouslyFailedImages);
- File.WriteAllText(failHistoryPath, text);
+ await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
}
numComplete++;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
new file mode 100644
index 000000000..acd4bf905
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Deletes path references from collections and playlists that no longer exists.
+/// </summary>
+public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
+{
+ private readonly ILocalizationManager _localization;
+ private readonly ICollectionManager _collectionManager;
+ private readonly IPlaylistManager _playlistManager;
+ private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class.
+ /// </summary>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
+ /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="providerManager">The provider manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ public CleanupCollectionAndPlaylistPathsTask(
+ ILocalizationManager localization,
+ ICollectionManager collectionManager,
+ IPlaylistManager playlistManager,
+ ILogger<CleanupCollectionAndPlaylistPathsTask> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem)
+ {
+ _localization = localization;
+ _collectionManager = collectionManager;
+ _playlistManager = playlistManager;
+ _logger = logger;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylists");
+
+ /// <inheritdoc />
+ public string Key => "CleanCollectionsAndPlaylists";
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylistsDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
+ if (collectionsFolder is null)
+ {
+ _logger.LogDebug("There is no collections folder to be found");
+ }
+ else
+ {
+ var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray();
+ _logger.LogDebug("Found {CollectionLength} boxsets", collections.Length);
+
+ for (var index = 0; index < collections.Length; index++)
+ {
+ var collection = collections[index];
+ _logger.LogDebug("Checking boxset {CollectionName}", collection.Name);
+
+ CleanupLinkedChildren(collection, cancellationToken);
+ progress.Report(50D / collections.Length * (index + 1));
+ }
+ }
+
+ var playlistsFolder = _playlistManager.GetPlaylistsFolder();
+ if (playlistsFolder is null)
+ {
+ _logger.LogDebug("There is no playlists folder to be found");
+ return;
+ }
+
+ var playlists = playlistsFolder.Children.OfType<Playlist>().ToArray();
+ _logger.LogDebug("Found {PlaylistLength} playlists", playlists.Length);
+
+ for (var index = 0; index < playlists.Length; index++)
+ {
+ var playlist = playlists[index];
+ _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name);
+
+ CleanupLinkedChildren(playlist, cancellationToken);
+ progress.Report(50D / playlists.Length * (index + 1));
+ }
+ }
+
+ private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken)
+ where T : Folder
+ {
+ List<LinkedChild>? itemsToRemove = null;
+ foreach (var linkedChild in folder.LinkedChildren)
+ {
+ if (!File.Exists(folder.Path))
+ {
+ _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path);
+ (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
+ }
+ }
+
+ if (itemsToRemove is not null)
+ {
+ _logger.LogDebug("Updating {FolderName}", folder.Name);
+ folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
+ folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
+
+ _providerManager.QueueRefresh(
+ folder.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = true
+ },
+ RefreshPriority.High);
+ }
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
deleted file mode 100644
index f78fc6f97..000000000
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
-
-/// <summary>
-/// Deletes Path references from collections that no longer exists.
-/// </summary>
-public class CleanupCollectionPathsTask : IScheduledTask
-{
- private readonly ILocalizationManager _localization;
- private readonly ICollectionManager _collectionManager;
- private readonly ILogger<CleanupCollectionPathsTask> _logger;
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CleanupCollectionPathsTask"/> class.
- /// </summary>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="providerManager">The provider manager.</param>
- /// <param name="fileSystem">The filesystem.</param>
- public CleanupCollectionPathsTask(
- ILocalizationManager localization,
- ICollectionManager collectionManager,
- ILogger<CleanupCollectionPathsTask> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem)
- {
- _localization = localization;
- _collectionManager = collectionManager;
- _logger = logger;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- }
-
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskCleanCollections");
-
- /// <inheritdoc />
- public string Key => "CleanCollections";
-
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription");
-
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
- /// <inheritdoc />
- public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
- if (collectionsFolder is null)
- {
- _logger.LogDebug("There is no collection folder to be found");
- return;
- }
-
- var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray();
- _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length);
-
- var itemsToRemove = new List<LinkedChild>();
- for (var index = 0; index < collections.Length; index++)
- {
- var collection = collections[index];
- _logger.LogDebug("Check Boxset {CollectionName}", collection.Name);
-
- foreach (var collectionLinkedChild in collection.LinkedChildren)
- {
- if (!File.Exists(collectionLinkedChild.Path))
- {
- _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path);
- itemsToRemove.Add(collectionLinkedChild);
- }
- }
-
- if (itemsToRemove.Count != 0)
- {
- _logger.LogDebug("Update Boxset {CollectionName}", collection.Name);
- collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray();
- await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken)
- .ConfigureAwait(false);
-
- _providerManager.QueueRefresh(
- collection.Id,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ForceSave = true
- },
- RefreshPriority.High);
-
- itemsToRemove.Clear();
- }
-
- progress.Report(100D / collections.Length * (index + 1));
- }
- }
-
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
- {
- return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 03ff96b19..e8e63d286 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -19,6 +19,7 @@ using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -36,6 +37,7 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -44,9 +46,10 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Class SessionManager.
/// </summary>
- public class SessionManager : ISessionManager, IDisposable
+ public sealed class SessionManager : ISessionManager, IAsyncDisposable
{
private readonly IUserDataManager _userDataManager;
+ private readonly IServerConfigurationManager _config;
private readonly ILogger<SessionManager> _logger;
private readonly IEventManager _eventManager;
private readonly ILibraryManager _libraryManager;
@@ -57,13 +60,12 @@ namespace Emby.Server.Implementations.Session
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
private readonly IDeviceManager _deviceManager;
-
- /// <summary>
- /// The active connections.
- /// </summary>
- private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase);
+ private readonly CancellationTokenRegistration _shutdownCallback;
+ private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
+ = new(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
+ private Timer _inactiveTimer;
private DtoOptions _itemInfoDtoOptions;
private bool _disposed = false;
@@ -72,6 +74,7 @@ namespace Emby.Server.Implementations.Session
ILogger<SessionManager> logger,
IEventManager eventManager,
IUserDataManager userDataManager,
+ IServerConfigurationManager config,
ILibraryManager libraryManager,
IUserManager userManager,
IMusicManager musicManager,
@@ -79,11 +82,13 @@ namespace Emby.Server.Implementations.Session
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager)
+ IMediaSourceManager mediaSourceManager,
+ IHostApplicationLifetime hostApplicationLifetime)
{
_logger = logger;
_eventManager = eventManager;
_userDataManager = userDataManager;
+ _config = config;
_libraryManager = libraryManager;
_userManager = userManager;
_musicManager = musicManager;
@@ -92,6 +97,7 @@ namespace Emby.Server.Implementations.Session
_appHost = appHost;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
+ _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
@@ -151,36 +157,6 @@ namespace Emby.Server.Implementations.Session
}
}
- /// <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)
- {
- _idleTimer?.Dispose();
- }
-
- _idleTimer = null;
-
- _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
-
- _disposed = true;
- }
-
private void CheckDisposed()
{
if (_disposed)
@@ -398,6 +374,15 @@ namespace Emby.Server.Implementations.Session
session.LastPlaybackCheckIn = DateTime.UtcNow;
}
+ if (info.IsPaused && session.LastPausedDate is null)
+ {
+ session.LastPausedDate = DateTime.UtcNow;
+ }
+ else if (!info.IsPaused)
+ {
+ session.LastPausedDate = null;
+ }
+
session.PlayState.IsPaused = info.IsPaused;
session.PlayState.PositionTicks = info.PositionTicks;
session.PlayState.MediaSourceId = info.MediaSourceId;
@@ -565,9 +550,18 @@ namespace Emby.Server.Implementations.Session
return users;
}
- private void StartIdleCheckTimer()
+ private void StartCheckTimers()
{
_idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
+
+ if (_config.Configuration.InactiveSessionThreshold > 0)
+ {
+ _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
+ }
+ else
+ {
+ StopInactiveCheckTimer();
+ }
}
private void StopIdleCheckTimer()
@@ -579,6 +573,15 @@ namespace Emby.Server.Implementations.Session
}
}
+ private void StopInactiveCheckTimer()
+ {
+ if (_inactiveTimer is not null)
+ {
+ _inactiveTimer.Dispose();
+ _inactiveTimer = null;
+ }
+ }
+
private async void CheckForIdlePlayback(object state)
{
var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
@@ -614,13 +617,50 @@ namespace Emby.Server.Implementations.Session
playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
.ToList();
}
-
- if (playingSessions.Count == 0)
+ else
{
StopIdleCheckTimer();
}
}
+ private async void CheckForInactiveSteams(object state)
+ {
+ var inactiveSessions = Sessions.Where(i =>
+ i.NowPlayingItem is not null
+ && i.PlayState.IsPaused
+ && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionThreshold);
+
+ foreach (var session in inactiveSessions)
+ {
+ _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold);
+
+ try
+ {
+ await SendPlaystateCommand(
+ session.Id,
+ session.Id,
+ new PlaystateRequest()
+ {
+ Command = PlaystateCommand.Stop,
+ ControllingUserId = session.UserId.ToString(),
+ SeekPositionTicks = session.PlayState?.PositionTicks
+ },
+ CancellationToken.None).ConfigureAwait(true);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", session.Id);
+ }
+ }
+
+ bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
+
+ if (!playingSessions)
+ {
+ StopInactiveCheckTimer();
+ }
+ }
+
private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
{
var item = session.FullNowPlayingItem;
@@ -697,7 +737,7 @@ namespace Emby.Server.Implementations.Session
eventArgs,
_logger);
- StartIdleCheckTimer();
+ StartCheckTimers();
}
/// <summary>
@@ -791,7 +831,7 @@ namespace Emby.Server.Implementations.Session
session.StartAutomaticProgress(info);
}
- StartIdleCheckTimer();
+ StartCheckTimers();
}
private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
@@ -980,27 +1020,27 @@ namespace Emby.Server.Implementations.Session
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
{
- bool playedToCompletion = false;
-
- if (!playbackFailed)
+ if (playbackFailed)
{
- var data = _userDataManager.GetUserData(user, item);
-
- if (positionTicks.HasValue)
- {
- playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
- }
- else
- {
- // If the client isn't able to report this, then we'll just have to make an assumption
- data.PlayCount++;
- data.Played = item.SupportsPlayedStatus;
- data.PlaybackPositionTicks = 0;
- playedToCompletion = true;
- }
+ return false;
+ }
- _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+ var data = _userDataManager.GetUserData(user, item);
+ bool playedToCompletion;
+ if (positionTicks.HasValue)
+ {
+ playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
}
+ else
+ {
+ // If the client isn't able to report this, then we'll just have to make an assumption
+ data.PlayCount++;
+ data.Played = item.SupportsPlayedStatus;
+ data.PlaybackPositionTicks = 0;
+ playedToCompletion = true;
+ }
+
+ _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
return playedToCompletion;
}
@@ -1331,32 +1371,6 @@ namespace Emby.Server.Implementations.Session
}
/// <summary>
- /// Sends the server shutdown notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendServerShutdownNotification(CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
- }
-
- /// <summary>
- /// Sends the server restart notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendServerRestartNotification(CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- _logger.LogDebug("Beginning SendServerRestartNotification");
-
- return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
- }
-
- /// <summary>
/// Adds the additional user.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
@@ -1439,10 +1453,15 @@ namespace Emby.Server.Implementations.Session
return AuthenticateNewSessionInternal(request, false);
}
- private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
+ internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
{
CheckDisposed();
+ ArgumentException.ThrowIfNullOrEmpty(request.App);
+ ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
+ ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
+ ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
+
User user = null;
if (!request.UserId.Equals(default))
{
@@ -1503,41 +1522,29 @@ namespace Emby.Server.Implementations.Session
return returnResult;
}
- private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
+ internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
+ // This should be validated above, but if it isn't don't delete all tokens.
+ ArgumentException.ThrowIfNullOrEmpty(deviceId);
+
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
DeviceId = deviceId,
- UserId = user.Id,
- Limit = 1
- }).ConfigureAwait(false)).Items.FirstOrDefault();
-
- var allExistingForDevice = (await _deviceManager.GetDevices(
- new DeviceQuery
- {
- DeviceId = deviceId
+ UserId = user.Id
}).ConfigureAwait(false)).Items;
- foreach (var auth in allExistingForDevice)
+ foreach (var auth in existing)
{
- if (existing is null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
+ try
{
- try
- {
- await Logout(auth).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error while logging out.");
- }
+ // Logout any existing sessions for the user on this device
+ await Logout(auth).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while logging out existing session.");
}
- }
-
- if (existing is not null)
- {
- _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken);
- return existing.AccessToken;
}
_logger.LogInformation("Creating new access token for user {0}", user.Id);
@@ -1848,5 +1855,59 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
+
+ /// <inheritdoc />
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ foreach (var session in _activeConnections.Values)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+
+ if (_idleTimer is not null)
+ {
+ await _idleTimer.DisposeAsync().ConfigureAwait(false);
+ _idleTimer = null;
+ }
+
+ if (_inactiveTimer is not null)
+ {
+ await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
+ _inactiveTimer = null;
+ }
+
+ await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
+
+ _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
+ _disposed = true;
+ }
+
+ private async void OnApplicationStopping()
+ {
+ _logger.LogInformation("Sending shutdown notifications");
+ try
+ {
+ var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown;
+
+ await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error sending server shutdown notifications");
+ }
+
+ // Close open websockets to allow Kestrel to shut down cleanly
+ foreach (var session in _activeConnections.Values)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+
+ _activeConnections.Clear();
+ }
}
}
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
new file mode 100644
index 000000000..2c477218f
--- /dev/null
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Hosting;
+
+namespace Emby.Server.Implementations;
+
+/// <inheritdoc />
+public class SystemManager : ISystemManager
+{
+ private readonly IHostApplicationLifetime _applicationLifetime;
+ private readonly IServerApplicationHost _applicationHost;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IStartupOptions _startupOptions;
+ private readonly IInstallationManager _installationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SystemManager"/> class.
+ /// </summary>
+ /// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param>
+ /// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param>
+ /// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param>
+ /// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param>
+ /// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param>
+ public SystemManager(
+ IHostApplicationLifetime applicationLifetime,
+ IServerApplicationHost applicationHost,
+ IServerApplicationPaths applicationPaths,
+ IServerConfigurationManager configurationManager,
+ IStartupOptions startupOptions,
+ IInstallationManager installationManager)
+ {
+ _applicationLifetime = applicationLifetime;
+ _applicationHost = applicationHost;
+ _applicationPaths = applicationPaths;
+ _configurationManager = configurationManager;
+ _startupOptions = startupOptions;
+ _installationManager = installationManager;
+ }
+
+ /// <inheritdoc />
+ public SystemInfo GetSystemInfo(HttpRequest request)
+ {
+ return new SystemInfo
+ {
+ HasPendingRestart = _applicationHost.HasPendingRestart,
+ IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
+ Version = _applicationHost.ApplicationVersionString,
+ WebSocketPortNumber = _applicationHost.HttpPort,
+ CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
+ Id = _applicationHost.SystemId,
+ ProgramDataPath = _applicationPaths.ProgramDataPath,
+ WebPath = _applicationPaths.WebPath,
+ LogPath = _applicationPaths.LogDirectoryPath,
+ ItemsByNamePath = _applicationPaths.InternalMetadataPath,
+ InternalMetadataPath = _applicationPaths.InternalMetadataPath,
+ CachePath = _applicationPaths.CachePath,
+ TranscodingTempPath = _configurationManager.GetTranscodePath(),
+ ServerName = _applicationHost.FriendlyName,
+ LocalAddress = _applicationHost.GetSmartApiUrl(request),
+ SupportsLibraryMonitor = true,
+ PackageName = _startupOptions.PackageName,
+ CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
+ };
+ }
+
+ /// <inheritdoc />
+ public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
+ {
+ return new PublicSystemInfo
+ {
+ Version = _applicationHost.ApplicationVersionString,
+ ProductName = _applicationHost.Name,
+ Id = _applicationHost.SystemId,
+ ServerName = _applicationHost.FriendlyName,
+ LocalAddress = _applicationHost.GetSmartApiUrl(request),
+ StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
+ };
+ }
+
+ /// <inheritdoc />
+ public void Restart() => ShutdownInternal(true);
+
+ /// <inheritdoc />
+ public void Shutdown() => ShutdownInternal(false);
+
+ private void ShutdownInternal(bool restart)
+ {
+ Task.Run(async () =>
+ {
+ await Task.Delay(100).ConfigureAwait(false);
+ _applicationHost.ShouldRestart = restart;
+ _applicationLifetime.StopApplication();
+ });
+ }
+}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index f0e173f0b..ef890aeb4 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -135,13 +135,13 @@ namespace Emby.Server.Implementations.TV
private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
{
- var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
+ var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, request.EnableResumable, false));
if (request.EnableRewatching)
{
- allNextUp = allNextUp.Concat(
- seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
- .OrderByDescending(i => i.LastWatchedDate);
+ allNextUp = allNextUp
+ .Concat(seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false, true)))
+ .OrderByDescending(i => i.LastWatchedDate);
}
// If viewing all next up for all series, remove first episodes
@@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.TV
/// Gets the next up.
/// </summary>
/// <returns>Task{Episode}.</returns>
- private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
+ private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool includeResumable, bool includePlayed)
{
var lastQuery = new InternalItemsQuery(user)
{
@@ -200,8 +200,8 @@ namespace Emby.Server.Implementations.TV
}
};
- // If rewatching is enabled, sort first by date played and then by season and episode numbers
- lastQuery.OrderBy = rewatching
+ // If including played results, sort first by date played and then by season and episode numbers
+ lastQuery.OrderBy = includePlayed
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
@@ -216,7 +216,7 @@ namespace Emby.Server.Implementations.TV
IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
Limit = 1,
- IsPlayed = rewatching,
+ IsPlayed = includePlayed,
IsVirtualItem = false,
ParentIndexNumberNotEquals = 0,
DtoOptions = dtoOptions
@@ -240,7 +240,7 @@ namespace Emby.Server.Implementations.TV
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
IncludeItemTypes = new[] { BaseItemKind.Episode },
- IsPlayed = rewatching,
+ IsPlayed = includePlayed,
IsVirtualItem = false,
DtoOptions = dtoOptions
})
@@ -269,7 +269,7 @@ namespace Emby.Server.Implementations.TV
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
}
- if (nextEpisode is not null)
+ if (nextEpisode is not null && !includeResumable)
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index a3bbd6df0..2d806c146 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -27,9 +27,9 @@ namespace Emby.Server.Implementations.Udp
private readonly byte[] _receiveBuffer = new byte[8192];
- private Socket _udpSocket;
- private IPEndPoint _endpoint;
- private bool _disposed = false;
+ private readonly Socket _udpSocket;
+ private readonly IPEndPoint _endpoint;
+ private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="UdpServer" /> class.
@@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp
_endpoint = new IPEndPoint(bindAddress, port);
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
+ {
+ MulticastLoopback = false,
+ };
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
@@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
try
{
+ _logger.LogDebug("Sending AutoDiscovery response");
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
}
catch (SocketException ex)
@@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
{
try
{
- var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false);
+ var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
+ var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{
@@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
}
catch (OperationCanceledException)
{
- // Don't throw
+ _logger.LogDebug("Broadcast socket operation cancelled");
}
}
}
@@ -125,9 +130,8 @@ namespace Emby.Server.Implementations.Udp
return;
}
- _udpSocket?.Dispose();
-
- GC.SuppressFinalize(this);
+ _udpSocket.Dispose();
+ _disposed = true;
}
}
}
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 6c198b6f9..c717744b1 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{
- var extension = Path.GetExtension(package.SourceUrl);
- if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
+ if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
@@ -521,10 +520,9 @@ namespace Emby.Server.Implementations.Updates
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
- using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested();
- var hash = Convert.ToHexString(md5.ComputeHash(stream));
+ var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
@@ -557,7 +555,7 @@ namespace Emby.Server.Implementations.Updates
reader.ExtractToDirectory(targetDir, true);
// Ensure we create one or populate existing ones with missing data.
- await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status);
+ await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir);
}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index 53841b0c4..02fdef150 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -84,4 +84,9 @@ public static class Policies
/// Policy name for managing LiveTV.
/// </summary>
public const string LiveTvManagement = "LiveTvManagement";
+
+ /// <summary>
+ /// Policy name for accessing subtitles management.
+ /// </summary>
+ public const string SubtitleManagement = "SubtitleManagement";
}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 95b296fae..42576934b 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Net.Mime;
using System.Threading.Tasks;
using Emby.Dlna;
-using Emby.Dlna.Main;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna;
@@ -33,12 +32,19 @@ public class DlnaServerController : BaseJellyfinApiController
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
/// </summary>
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public DlnaServerController(IDlnaManager dlnaManager)
+ /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param>
+ /// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param>
+ /// <param name="mediaReceiverRegistrar">Instance of the <see cref="IMediaReceiverRegistrar"/> interface.</param>
+ public DlnaServerController(
+ IDlnaManager dlnaManager,
+ IContentDirectory contentDirectory,
+ IConnectionManager connectionManager,
+ IMediaReceiverRegistrar mediaReceiverRegistrar)
{
_dlnaManager = dlnaManager;
- _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
- _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
- _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
+ _contentDirectory = contentDirectory;
+ _connectionManager = connectionManager;
+ _mediaReceiverRegistrar = mediaReceiverRegistrar;
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index ce684e457..38953dc21 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
private const string DefaultEventEncoderPreset = "superfast";
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+ private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
+
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
@@ -408,6 +410,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
[HttpGet("Videos/{itemId}/master.m3u8")]
@@ -465,7 +468,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableTrickplay = true)
{
var streamingRequest = new HlsVideoRequestDto
{
@@ -519,7 +523,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
+ EnableTrickplay = enableTrickplay
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -1651,7 +1656,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
threads,
mapArgs,
- GetVideoArguments(state, startNumber, isEventPlaylist),
+ GetVideoArguments(state, startNumber, isEventPlaylist, segmentContainer),
GetAudioArguments(state),
maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
@@ -1703,19 +1708,33 @@ public class DynamicHlsController : BaseJellyfinApiController
}
var audioCodec = _encodingHelper.GetAudioEncoder(state);
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+ // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
+ var strictArgs = string.Empty;
+ var actualOutputAudioCodec = state.ActualOutputAudioCodec;
+ if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
+ || (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
+ {
+ strictArgs = " -strict -2";
+ }
if (!state.IsOutputVideo)
{
+ var audioTranscodeParams = string.Empty;
+
+ // -vn to drop any video streams
+ audioTranscodeParams += "-vn";
+
if (EncodingHelper.IsCopyCodec(audioCodec))
{
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
-
- return "-acodec copy -strict -2" + bitStreamArgs;
+ return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
}
- var audioTranscodeParams = string.Empty;
-
- audioTranscodeParams += "-acodec " + audioCodec;
+ audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
var audioBitrate = state.OutputAudioBitrate;
var audioChannels = state.OutputAudioChannels;
@@ -1743,25 +1762,12 @@ public class DynamicHlsController : BaseJellyfinApiController
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- audioTranscodeParams += " -vn";
return audioTranscodeParams;
}
- // dts, flac, opus and truehd are experimental in mp4 muxer
- var strictArgs = string.Empty;
- var actualOutputAudioCodec = state.ActualOutputAudioCodec;
- if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
- {
- strictArgs = " -strict -2";
- }
-
if (EncodingHelper.IsCopyCodec(audioCodec))
{
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
@@ -1772,7 +1778,7 @@ public class DynamicHlsController : BaseJellyfinApiController
return copyArgs;
}
- var args = "-codec:a:0 " + audioCodec + strictArgs;
+ var args = "-codec:a:0 " + audioCodec + bitStreamArgs + strictArgs;
var channels = state.OutputAudioChannels;
@@ -1816,8 +1822,9 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="state">The <see cref="StreamState"/>.</param>
/// <param name="startNumber">The first number in the hls sequence.</param>
/// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param>
+ /// <param name="segmentContainer">The segment container.</param>
/// <returns>The command line arguments for video transcoding.</returns>
- private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist)
+ private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist, string segmentContainer)
{
if (state.VideoStream is null)
{
@@ -1909,7 +1916,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
// TODO why was this not enabled for VOD?
- if (isEventPlaylist)
+ if (isEventPlaylist && string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
{
args += " -flags -global_header";
}
@@ -2042,9 +2049,9 @@ public class DynamicHlsController : BaseJellyfinApiController
return null;
}
- var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+ var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
- var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+ var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index d7cec865e..6eedfd8c7 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
// TODO: Deprecate with new iOS app
- var file = segmentId + Path.GetExtension(Request.Path);
+ var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
@@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
- var file = playlistId + Path.GetExtension(Request.Path);
+ var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
+ || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Invalid segment.");
}
@@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromRoute, Required] string segmentId,
[FromRoute, Required] string segmentContainer)
{
- var file = segmentId + Path.GetExtension(Request.Path);
+ var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 3c5f18af5..7b10ea170 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -7,6 +7,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
_appPaths = appPaths;
}
+ private static Stream GetFromBase64Stream(Stream inputStream)
+ => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
+
/// <summary>
/// Sets the user image.
/// </summary>
@@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
@@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await using (fs.ConfigureAwait(false))
{
- await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
+ await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
}
return NoContent();
@@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
return NoContent();
}
- private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
- {
- using var reader = new StreamReader(inputStream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- var bytes = Convert.FromBase64String(text);
- return new MemoryStream(bytes, 0, bytes.Length, false, true);
- }
-
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
{
int? width = null;
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 46c0a8d52..21941ff94 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController
return new AllThemeMediaResult
{
- ThemeSongsResult = themeSongs?.Value,
- ThemeVideosResult = themeVideos?.Value,
+ ThemeSongsResult = themeSongs.Value,
+ ThemeVideosResult = themeVideos.Value,
SoundtrackSongsResult = new ThemeMediaResult()
};
}
@@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
- parent = parent?.GetParent();
+ parent = parent.GetParent();
}
return baseItemDtos;
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 267ba4afb..649397d68 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
@@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
public LiveTvController(
ILiveTvManager liveTvManager,
IUserManager userManager,
@@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController
IDtoService dtoService,
IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager,
- TranscodingJobHelper transcodingJobHelper,
- ISessionManager sessionManager)
+ TranscodingJobHelper transcodingJobHelper)
{
_liveTvManager = liveTvManager;
_userManager = userManager;
@@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController
_mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager;
_transcodingJobHelper = transcodingJobHelper;
- _sessionManager = sessionManager;
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 7d02550b6..c9e256af3 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -114,7 +115,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="200">Subtitles retrieved.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -134,7 +135,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="204">Subtitle downloaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -398,16 +399,15 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="204">Subtitle uploaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")]
- [Authorize(Policy = Policies.RequiresElevation)]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId,
[FromBody, Required] UploadSubtitleDto body)
{
var video = (Video)_libraryManager.GetItemById(itemId);
- var data = Convert.FromBase64String(body.Data);
- var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
+ await using (stream.ConfigureAwait(false))
{
await _subtitleManager.UploadSubtitle(
video,
@@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
Language = body.Language,
IsForced = body.IsForced,
IsHearingImpaired = body.IsHearingImpaired,
- Stream = memoryStream
+ Stream = stream
}).ConfigureAwait(false);
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index a29790961..11095a97f 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -4,14 +4,12 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Mime;
-using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.System;
@@ -27,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
/// </summary>
public class SystemController : BaseJellyfinApiController
{
+ private readonly ILogger<SystemController> _logger;
private readonly IServerApplicationHost _appHost;
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
- private readonly INetworkManager _network;
- private readonly ILogger<SystemController> _logger;
+ private readonly INetworkManager _networkManager;
+ private readonly ISystemManager _systemManager;
/// <summary>
/// Initializes a new instance of the <see cref="SystemController"/> class.
/// </summary>
- /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+ /// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
- /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
- /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+ /// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
+ /// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
public SystemController(
- IServerConfigurationManager serverConfigurationManager,
+ ILogger<SystemController> logger,
IServerApplicationHost appHost,
+ IServerApplicationPaths appPaths,
IFileSystem fileSystem,
- INetworkManager network,
- ILogger<SystemController> logger)
+ INetworkManager networkManager,
+ ISystemManager systemManager)
{
- _appPaths = serverConfigurationManager.ApplicationPaths;
+ _logger = logger;
_appHost = appHost;
+ _appPaths = appPaths;
_fileSystem = fileSystem;
- _network = network;
- _logger = logger;
+ _networkManager = networkManager;
+ _systemManager = systemManager;
}
/// <summary>
@@ -66,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult<SystemInfo> GetSystemInfo()
- {
- return _appHost.GetSystemInfo(Request);
- }
+ => _systemManager.GetSystemInfo(Request);
/// <summary>
/// Gets public information about the server.
@@ -78,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
[HttpGet("Info/Public")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
- {
- return _appHost.GetPublicSystemInfo(Request);
- }
+ => _systemManager.GetPublicSystemInfo(Request);
/// <summary>
/// Pings the system.
@@ -91,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
[HttpPost("Ping", Name = "PostPingSystem")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<string> PingSystem()
- {
- return _appHost.Name;
- }
+ => _appHost.Name;
/// <summary>
/// Restarts the application.
@@ -107,11 +103,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult RestartApplication()
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- _appHost.Restart();
- });
+ _systemManager.Restart();
return NoContent();
}
@@ -127,11 +119,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult ShutdownApplication()
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- await _appHost.Shutdown().ConfigureAwait(false);
- });
+ _systemManager.Shutdown();
return NoContent();
}
@@ -189,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
return new EndPointInfo
{
IsLocal = HttpContext.IsLocal(),
- IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
+ IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
};
}
@@ -227,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
{
- var result = _network.GetMacAddresses()
+ var result = _networkManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i));
return Ok(result);
}
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
new file mode 100644
index 000000000..2dc960229
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -0,0 +1,101 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Trickplay controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class TrickplayController : BaseJellyfinApiController
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ITrickplayManager _trickplayManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayController"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param>
+ /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
+ public TrickplayController(
+ ILibraryManager libraryManager,
+ ITrickplayManager trickplayManager)
+ {
+ _libraryManager = libraryManager;
+ _trickplayManager = trickplayManager;
+ }
+
+ /// <summary>
+ /// Gets an image tiles playlist for trickplay.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="width">The width of a single tile.</param>
+ /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
+ /// <response code="200">Tiles playlist returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the trickplay playlist file.</returns>
+ [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetTrickplayHlsPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int width,
+ [FromQuery] Guid? mediaSourceId)
+ {
+ string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(playlist))
+ {
+ return NotFound();
+ }
+
+ return Content(playlist, MimeTypes.GetMimeType("playlist.m3u8"), Encoding.UTF8);
+ }
+
+ /// <summary>
+ /// Gets a trickplay tile image.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="width">The width of a single tile.</param>
+ /// <param name="index">The index of the desired tile.</param>
+ /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
+ /// <response code="200">Tile image returned.</response>
+ /// <response code="200">Tile image not found at specified index.</response>
+ /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns>
+ [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public ActionResult GetTrickplayTileImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int width,
+ [FromRoute, Required] int index,
+ [FromQuery] Guid? mediaSourceId)
+ {
+ var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
+ if (System.IO.File.Exists(path))
+ {
+ return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
+ }
+
+ return NotFound();
+ }
+}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 7d23281f2..af403fa80 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -68,7 +68,8 @@ public class TvShowsController : BaseJellyfinApiController
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
- /// <param name="enableRewatching">Whether to include watched episode in next up results.</param>
+ /// <param name="enableResumable">Whether to include resumable episodes in next up results.</param>
+ /// <param name="enableRewatching">Whether to include watched episodes in next up results.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
[HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -86,6 +87,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false,
+ [FromQuery] bool enableResumable = true,
[FromQuery] bool enableRewatching = false)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -104,6 +106,7 @@ public class TvShowsController : BaseJellyfinApiController
EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode,
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
+ EnableResumable = enableResumable,
EnableRewatching = enableRewatching
},
options);
@@ -132,7 +135,7 @@ public class TvShowsController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the upcoming episodes.</returns>
[HttpGet("Upcoming")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 63667e7e6..24082fcff 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
@@ -19,6 +20,7 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Net;
@@ -46,6 +48,7 @@ public class DynamicHlsHelper
private readonly ILogger<DynamicHlsHelper> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly EncodingHelper _encodingHelper;
+ private readonly ITrickplayManager _trickplayManager;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
@@ -62,6 +65,7 @@ public class DynamicHlsHelper
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
public DynamicHlsHelper(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -74,7 +78,8 @@ public class DynamicHlsHelper
INetworkManager networkManager,
ILogger<DynamicHlsHelper> logger,
IHttpContextAccessor httpContextAccessor,
- EncodingHelper encodingHelper)
+ EncodingHelper encodingHelper,
+ ITrickplayManager trickplayManager)
{
_libraryManager = libraryManager;
_userManager = userManager;
@@ -88,6 +93,7 @@ public class DynamicHlsHelper
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_encodingHelper = encodingHelper;
+ _trickplayManager = trickplayManager;
}
/// <summary>
@@ -200,13 +206,6 @@ public class DynamicHlsHelper
if (state.VideoStream is not null && state.VideoRequest is not null)
{
- // Provide a workaround for the case issue between flac and fLaC.
- var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
-
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
// Provide SDR HEVC entrance for backward compatibility.
@@ -236,14 +235,7 @@ public class DynamicHlsHelper
}
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
- var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
-
- // Provide a workaround for the case issue between flac and fLaC.
- flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
+ AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Restore the video codec
state.OutputVideoCodec = "copy";
@@ -274,13 +266,6 @@ public class DynamicHlsHelper
state.VideoStream.Level = originalLevel;
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
builder.Append(newPlaylist);
-
- // Provide a workaround for the case issue between flac and fLaC.
- flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
}
}
@@ -301,6 +286,13 @@ public class DynamicHlsHelper
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
+ if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
+ {
+ var sourceId = Guid.Parse(state.Request.MediaSourceId);
+ var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
+ AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
+ }
+
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
@@ -530,6 +522,41 @@ public class DynamicHlsHelper
}
/// <summary>
+ /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
+ /// </summary>
+ /// <param name="state">StreamState of the current stream.</param>
+ /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
+ /// <param name="builder">StringBuilder to append the field to.</param>
+ /// <param name="user">Http user context.</param>
+ private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder builder, ClaimsPrincipal user)
+ {
+ const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\"";
+
+ foreach (var resolution in trickplayResolutions)
+ {
+ var width = resolution.Key;
+ var trickplayInfo = resolution.Value;
+
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
+ width.ToString(CultureInfo.InvariantCulture),
+ state.Request.MediaSourceId,
+ user.GetToken());
+
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ playlistFormat,
+ trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
+ url);
+
+ builder.AppendLine();
+ }
+ }
+
+ /// <summary>
/// Get the H.26X level of the output video stream.
/// </summary>
/// <param name="state">StreamState of the current stream.</param>
@@ -693,7 +720,7 @@ public class DynamicHlsHelper
// Currently we only transcode to 8 bits AV1
int bitDepth = 8;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && state.VideoStream != null
+ && state.VideoStream is not null
&& state.VideoStream.BitDepth.HasValue)
{
bitDepth = state.VideoStream.BitDepth.Value;
@@ -767,16 +794,4 @@ public class DynamicHlsHelper
newValue.ToString(),
StringComparison.Ordinal);
}
-
- private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
- {
- if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
- {
- return string.Empty;
- }
-
- var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
-
- return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
- }
}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 9a141a16d..5eec1b0ca 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -5,7 +5,9 @@ using System.Text;
namespace Jellyfin.Api.Helpers;
/// <summary>
-/// Hls Codec string helpers.
+/// Helpers to generate HLS codec strings according to
+/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a>
+/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>.
/// </summary>
public static class HlsCodecStringHelpers
{
@@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
/// <summary>
/// Codec name for FLAC.
/// </summary>
- public const string FLAC = "flac";
+ public const string FLAC = "fLaC";
/// <summary>
/// Codec name for ALAC.
@@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
/// <summary>
/// Codec name for OPUS.
/// </summary>
- public const string OPUS = "opus";
+ public const string OPUS = "Opus";
/// <summary>
/// Gets a MP3 codec string.
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 782cd6568..11f6bcf6b 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -191,6 +191,11 @@ public static class StreamingHelpers
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
}
+ if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
+ {
+ containerInternal = ".pcm";
+ }
+
state.OutputAudioCodec = outputAudioCodec;
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
@@ -243,7 +248,7 @@ public static class StreamingHelpers
? GetOutputFileExtension(state, mediaSource)
: ("." + state.OutputContainer);
- state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
+ state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
return state;
}
@@ -416,10 +421,9 @@ public static class StreamingHelpers
/// <param name="state">The state.</param>
/// <param name="mediaSource">The mediaSource.</param>
/// <returns>System.String.</returns>
- private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
+ private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
{
var ext = Path.GetExtension(state.RequestedUrl);
-
if (!string.IsNullOrEmpty(ext))
{
return ext;
@@ -458,10 +462,9 @@ public static class StreamingHelpers
return ".asf";
}
}
-
- // Try to infer based on the desired audio codec
- if (!state.IsVideoRequest)
+ else
{
+ // Try to infer based on the desired audio codec
var audioCodec = state.Request.AudioCodec;
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
@@ -492,7 +495,7 @@ public static class StreamingHelpers
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
}
- return null;
+ throw new InvalidOperationException("Failed to find an appropriate file extension");
}
/// <summary>
@@ -509,7 +512,7 @@ public static class StreamingHelpers
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- var ext = outputFileExtension?.ToLowerInvariant();
+ var ext = outputFileExtension.ToLowerInvariant();
var folder = serverConfigurationManager.GetTranscodePath();
return Path.Combine(folder, filename + ext);
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index cee8e0f9b..c16a586d6 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
}
- if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
+ if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
{
string subtitlePath = state.SubtitleStream.Path;
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
@@ -620,7 +620,7 @@ public class TranscodingJobHelper : IDisposable
state.TranscodingJob = transcodingJob;
// Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
- _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
+ _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
// Wait for the file to exist before proceeding
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 6a0a4706b..03dd97367 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -8,8 +8,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
- <NoWarn>AD0001</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -26,8 +24,12 @@
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
index 060c14f89..acbb4877d 100644
--- a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
@@ -122,17 +122,17 @@ public class ExceptionMiddleware
private static int GetStatusCode(Exception ex)
{
- switch (ex)
+ return ex switch
{
- case ArgumentException _: return StatusCodes.Status400BadRequest;
- case AuthenticationException _: return StatusCodes.Status401Unauthorized;
- case SecurityException _: return StatusCodes.Status403Forbidden;
- case DirectoryNotFoundException _:
- case FileNotFoundException _:
- case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
- case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
- default: return StatusCodes.Status500InternalServerError;
- }
+ ArgumentException => StatusCodes.Status400BadRequest,
+ AuthenticationException => StatusCodes.Status401Unauthorized,
+ SecurityException => StatusCodes.Status403Forbidden,
+ DirectoryNotFoundException => StatusCodes.Status404NotFound,
+ FileNotFoundException => StatusCodes.Status404NotFound,
+ ResourceNotFoundException => StatusCodes.Status404NotFound,
+ MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed,
+ _ => StatusCodes.Status500InternalServerError
+ };
}
private string NormalizeExceptionMessage(string msg)
diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
index 8bf626035..acf3645fd 100644
--- a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
@@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
- var localPath = httpContext.Request.Path.ToString();
- if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
+ if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
httpContext.Response.Redirect("web/robots.txt");
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
index a34fd01d5..3e3604b2a 100644
--- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
@@ -77,7 +77,7 @@ public class CommaDelimitedArrayModelBinder : IModelBinder
var typedValueIndex = 0;
for (var i = 0; i < parsedValues.Length; i++)
{
- if (parsedValues[i] != null)
+ if (parsedValues[i] is not null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
index cb9a82955..ae9f0a8cd 100644
--- a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
@@ -77,7 +77,7 @@ public class PipeDelimitedArrayModelBinder : IModelBinder
var typedValueIndex = 0;
for (var i = 0; i < parsedValues.Length; i++)
{
- if (parsedValues[i] != null)
+ if (parsedValues[i] is not null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
index 60c529d4a..8548fec1a 100644
--- a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
@@ -1,4 +1,4 @@
-namespace Jellyfin.Api.Models.StreamingDtos;
+namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary>
/// The video request dto.
@@ -15,4 +15,9 @@ public class VideoRequestDto : StreamingRequestDto
/// Gets or sets a value indicating whether to enable subtitles in the manifest.
/// </summary>
public bool EnableSubtitlesInManifest { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable trickplay images.
+ /// </summary>
+ public bool EnableTrickplay { get; set; }
}
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 5b90d65d8..ba228cb00 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -53,7 +53,10 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
/// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _activityManager.EntryCreated -= OnEntryCreated;
+ if (dispose)
+ {
+ _activityManager.EntryCreated -= OnEntryCreated;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index a9df2d671..37c108d5a 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -58,8 +58,11 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn
/// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _taskManager.TaskExecuting -= OnTaskExecuting;
- _taskManager.TaskCompleted -= OnTaskCompleted;
+ if (dispose)
+ {
+ _taskManager.TaskExecuting -= OnTaskExecuting;
+ _taskManager.TaskCompleted -= OnTaskCompleted;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index b403ff46d..3c2b86142 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -57,13 +57,16 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
/// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
- _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
- _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
- _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
- _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
- _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
- _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ if (dispose)
+ {
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs
new file mode 100644
index 000000000..64e7da1b5
--- /dev/null
+++ b/Jellyfin.Data/Entities/TrickplayInfo.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// An entity representing the metadata for a group of trickplay tiles.
+/// </summary>
+public class TrickplayInfo
+{
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [JsonIgnore]
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets width of an individual thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets height of an individual thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets amount of thumbnails per row.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int TileWidth { get; set; }
+
+ /// <summary>
+ /// Gets or sets amount of thumbnails per column.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int TileHeight { get; set; }
+
+ /// <summary>
+ /// Gets or sets total amount of non-black thumbnails.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int ThumbnailCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets interval in milliseconds between each trickplay thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Interval { get; set; }
+
+ /// <summary>
+ /// Gets or sets peak bandwith usage in bits per second.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Bandwidth { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 58ddaaf83..ea0de3016 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities
/// </summary>
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
+ /// <summary>
+ /// Gets or sets the cast receiver id.
+ /// </summary>
+ [StringLength(32)]
+ public string? CastReceiverId { get; set; }
+
/// <inheritdoc />
[ConcurrencyCheck]
public uint RowVersion { get; private set; }
@@ -499,6 +505,7 @@ namespace Jellyfin.Data.Entities
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
+ Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false));
}
/// <summary>
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
index 40280b95e..6644f0151 100644
--- a/Jellyfin.Data/Enums/PermissionKind.cs
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -113,6 +113,11 @@ namespace Jellyfin.Data.Enums
/// <summary>
/// Whether the user can create, modify and delete collections.
/// </summary>
- EnableCollectionManagement = 21
+ EnableCollectionManagement = 21,
+
+ /// <summary>
+ /// Whether the user can edit subtitles.
+ /// </summary>
+ EnableSubtitleManagement = 22
}
}
diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs
index 10a805666..29308789a 100644
--- a/Jellyfin.Data/Enums/PersonKind.cs
+++ b/Jellyfin.Data/Enums/PersonKind.cs
@@ -94,4 +94,40 @@ public enum PersonKind
/// A person who was the illustrator.
/// </summary>
Illustrator,
+
+ /// <summary>
+ /// A person responsible for drawing the art.
+ /// </summary>
+ Penciller,
+
+ /// <summary>
+ /// A person responsible for inking the pencil art.
+ /// </summary>
+ Inker,
+
+ /// <summary>
+ /// A person responsible for applying color to drawings.
+ /// </summary>
+ Colorist,
+
+ /// <summary>
+ /// A person responsible for drawing text and speech bubbles.
+ /// </summary>
+ Letterer,
+
+ /// <summary>
+ /// A person responsible for drawing the cover art.
+ /// </summary>
+ CoverArtist,
+
+ /// <summary>
+ /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter.
+ /// An editor may also prepare a resource for production, publication, or distribution.
+ /// </summary>
+ Editor,
+
+ /// <summary>
+ /// A person who renders a text from one language into another.
+ /// </summary>
+ Translator
}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 1bc5d8bf9..847853ca0 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -27,8 +27,12 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
- <!-- Code analysers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
index 573c36be7..90ebcd390 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
@@ -164,7 +164,7 @@ namespace Jellyfin.Networking.Configuration
public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
/// <summary>
- /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>.
+ /// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>.
/// </summary>
public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs
index e45fa3bcb..a1e1140f1 100644
--- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs
+++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
@@ -204,7 +203,7 @@ public static partial class NetworkExtensions
{
var ipBlock = splitString.Current;
var address = IPAddress.None;
- if (negated && ipBlock.StartsWith<char>("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
+ if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
{
address = tmpAddress;
}
@@ -231,12 +230,12 @@ public static partial class NetworkExtensions
}
else if (address.AddressFamily == AddressFamily.InterNetwork)
{
- result = new IPNetwork(address, Network.MinimumIPv4PrefixSize);
+ result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
return true;
}
else if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
- result = new IPNetwork(address, Network.MinimumIPv6PrefixSize);
+ result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
return true;
}
}
@@ -284,12 +283,15 @@ public static partial class NetworkExtensions
if (hosts.Count <= 2)
{
+ var firstPart = hosts[0];
+
// Is hostname or hostname:port
- if (FqdnGeneratedRegex().IsMatch(hosts[0]))
+ if (FqdnGeneratedRegex().IsMatch(firstPart))
{
try
{
- addresses = Dns.GetHostAddresses(hosts[0]);
+ // .NET automatically filters only supported returned addresses based on OS support.
+ addresses = Dns.GetHostAddresses(firstPart);
return true;
}
catch (SocketException)
@@ -299,7 +301,7 @@ public static partial class NetworkExtensions
}
// Is an IPv4 or IPv4:port
- if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address))
+ if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
{
if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index 4cff5927f..43d08c37a 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -9,8 +9,12 @@
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index f20e28526..9c59500d7 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -15,7 +15,9 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Networking.Manager
{
@@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager
private readonly IConfigurationManager _configurationManager;
+ private readonly IConfiguration _startupConfig;
+
private readonly object _networkEventLock;
/// <summary>
/// Holds the published server URLs and the IPs to use them on.
/// </summary>
- private IReadOnlyDictionary<IPData, string> _publishedServerUrls;
+ private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
private IReadOnlyList<IPNetwork> _remoteAddressFilter;
@@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager
/// <summary>
/// Initializes a new instance of the <see cref="NetworkManager"/> class.
/// </summary>
- /// <param name="configurationManager">IServerConfigurationManager instance.</param>
+ /// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param>
+ /// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param>
/// <param name="logger">Logger to use for messages.</param>
#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
- public NetworkManager(IConfigurationManager configurationManager, ILogger<NetworkManager> logger)
+ public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(configurationManager);
_logger = logger;
_configurationManager = configurationManager;
+ _startupConfig = startupConfig;
_initLock = new();
_interfaces = new List<IPData>();
_macAddresses = new List<PhysicalAddress>();
- _publishedServerUrls = new Dictionary<IPData, string>();
+ _publishedServerUrls = new List<PublishedServerUriOverride>();
_networkEventLock = new object();
_remoteAddressFilter = new List<IPNetwork>();
@@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager
/// <summary>
/// Gets the Published server override list.
/// </summary>
- public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls;
+ public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
/// <inheritdoc/>
public void Dispose()
@@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager
{
if (!_eventfire)
{
- _logger.LogDebug("Network Address Change Event.");
// As network events tend to fire one after the other only fire once every second.
_eventfire = true;
OnNetworkChange();
@@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager
}
else
{
- InitialiseInterfaces();
- InitialiseLan(networkConfig);
+ InitializeInterfaces();
+ InitializeLan(networkConfig);
EnforceBindSettings(networkConfig);
}
+ PrintNetworkInformation(networkConfig);
NetworkChanged?.Invoke(this, EventArgs.Empty);
}
finally
@@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
/// Generate a list of all active mac addresses that aren't loopback addresses.
/// </summary>
- private void InitialiseInterfaces()
+ private void InitializeInterfaces()
{
lock (_initLock)
{
@@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager
try
{
var nics = NetworkInterface.GetAllNetworkInterfaces()
- .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up);
+ .Where(i => i.OperationalStatus == OperationalStatus.Up);
foreach (NetworkInterface adapter in nics)
{
@@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager
{
if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
{
- var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
- interfaceObject.Index = ipProperties.GetIPv4Properties().Index;
- interfaceObject.Name = adapter.Name;
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv4Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
interfaces.Add(interfaceObject);
}
else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
{
- var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
- interfaceObject.Index = ipProperties.GetIPv6Properties().Index;
- interfaceObject.Name = adapter.Name;
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv6Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
interfaces.Add(interfaceObject);
}
}
}
-#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
{
// Ignore error, and attempt to continue.
_logger.LogError(ex, "Error encountered parsing interfaces.");
}
}
}
-#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error obtaining interfaces.");
}
@@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager
{
_logger.LogWarning("No interface information available. Using loopback interface(s).");
- if (IsIPv4Enabled && !IsIPv6Enabled)
+ if (IsIPv4Enabled)
{
- interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo"));
+ interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
}
- if (!IsIPv4Enabled && IsIPv6Enabled)
+ if (IsIPv6Enabled)
{
- interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo"));
+ interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
}
}
@@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager
}
/// <summary>
- /// Initialises internal LAN cache.
+ /// Initializes internal LAN cache.
/// </summary>
- private void InitialiseLan(NetworkConfiguration config)
+ private void InitializeLan(NetworkConfiguration config)
{
lock (_initLock)
{
@@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager
_excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
? excludedSubnets
: new List<IPNetwork>();
-
- _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
}
}
@@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager
.ToHashSet();
interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
- if (bindAddresses.Contains(IPAddress.Loopback))
+ if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
{
interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
}
- if (bindAddresses.Contains(IPAddress.IPv6Loopback))
+ if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
{
interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
}
@@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
}
- _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
_interfaces = interfaces;
}
}
/// <summary>
- /// Initialises the remote address values.
+ /// Initializes the remote address values.
/// </summary>
- private void InitialiseRemote(NetworkConfiguration config)
+ private void InitializeRemote(NetworkConfiguration config)
{
lock (_initLock)
{
@@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager
/// format is subnet=ipaddress|host|uri
/// when subnet = 0.0.0.0, any external address matches.
/// </summary>
- private void InitialiseOverrides(NetworkConfiguration config)
+ private void InitializeOverrides(NetworkConfiguration config)
{
lock (_initLock)
{
- var publishedServerUrls = new Dictionary<IPData, string>();
- var overrides = config.PublishedServerUriBySubnet;
+ var publishedServerUrls = new List<PublishedServerUriOverride>();
+
+ // Prefer startup configuration.
+ var startupOverrideKey = _startupConfig[AddressOverrideKey];
+ if (!string.IsNullOrEmpty(startupOverrideKey))
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ startupOverrideKey,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ startupOverrideKey,
+ true,
+ true));
+ _publishedServerUrls = publishedServerUrls;
+ return;
+ }
+ var overrides = config.PublishedServerUriBySubnet;
foreach (var entry in overrides)
{
var parts = entry.Split('=');
@@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager
var identifier = parts[0];
if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
{
- publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement;
+ // Drop any other overrides in case an "all" override exists
+ publishedServerUrls.Clear();
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ replacement,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ replacement,
+ true,
+ true));
+ break;
}
else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
{
- publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement;
- publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ replacement,
+ false,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ replacement,
+ false,
+ true));
}
else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
{
foreach (var lan in _lanSubnets)
{
var lanPrefix = lan.Prefix;
- publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
+ replacement,
+ true,
+ false));
}
}
else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
{
var data = new IPData(result.Prefix, result);
- publishedServerUrls[data] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ data,
+ replacement,
+ true,
+ true));
}
else if (TryParseInterface(identifier, out var ifaces))
{
foreach (var iface in ifaces)
{
- publishedServerUrls[iface] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ iface,
+ replacement,
+ true,
+ true));
}
}
else
@@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager
}
/// <summary>
- /// Reloads all settings and re-initialises the instance.
+ /// Reloads all settings and re-Initializes the instance.
/// </summary>
/// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
public void UpdateSettings(object configuration)
@@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager
var config = (NetworkConfiguration)configuration;
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
- InitialiseLan(config);
- InitialiseRemote(config);
+ InitializeLan(config);
+ InitializeRemote(config);
if (string.IsNullOrEmpty(MockNetworkSettings))
{
- InitialiseInterfaces();
+ InitializeInterfaces();
}
else // Used in testing only.
{
@@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
{
- var data = new IPData(address, subnet, parts[2]);
- data.Index = index;
+ var data = new IPData(address, subnet, parts[2])
+ {
+ Index = index
+ };
interfaces.Add(data);
}
}
@@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager
}
EnforceBindSettings(config);
- InitialiseOverrides(config);
+ InitializeOverrides(config);
+
+ PrintNetworkInformation(config, false);
}
/// <summary>
@@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager
/// <inheritdoc/>
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
{
- if (_interfaces.Count != 0)
+ if (_interfaces.Count > 0 || individualInterfaces)
{
return _interfaces;
}
// No bind address and no exclusions, so listen on all interfaces.
var result = new List<IPData>();
-
- if (individualInterfaces)
- {
- result.AddRange(_interfaces);
- return result;
- }
-
if (IsIPv4Enabled && IsIPv6Enabled)
{
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
@@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager
bindPreference = string.Empty;
int? port = null;
- var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)
- || x.Key.Address.Equals(IPAddress.IPv6Any)
- || x.Key.Subnet.Contains(source))
- .DistinctBy(x => x.Key)
- .OrderBy(x => x.Key.Address.Equals(IPAddress.Any)
- || x.Key.Address.Equals(IPAddress.IPv6Any))
+ // Only consider subnets including the source IP, prefering specific overrides
+ List<PublishedServerUriOverride> validPublishedServerUrls;
+ if (!isInExternalSubnet)
+ {
+ // Only use matching internal subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+ .ToList();
+ }
+ else
+ {
+ // Only use matching external subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList();
+ }
- // Check for user override.
foreach (var data in validPublishedServerUrls)
{
- if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any)))
- {
- // External.
- bindPreference = data.Value;
- break;
- }
-
- // Get address interface.
- var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address));
+ // Get interface matching override subnet
+ var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
if (intf?.Address is not null)
{
- // Match IP address.
- bindPreference = data.Value;
+ // If matching interface is found, use override
+ bindPreference = data.OverrideUri;
break;
}
}
@@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager
return false;
}
- // Has it got a port defined?
+ // Handle override specifying port
var parts = bindPreference.Split(':');
if (parts.Length > 1)
{
@@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager
{
bindPreference = parts[0];
port = p;
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
+ return true;
}
}
- if (port is not null)
- {
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
- }
- else
- {
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
- }
-
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
return true;
}
@@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
return true;
}
+
+ private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
+ {
+ var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
+ if (_logger.IsEnabled(logLevel))
+ {
+ _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
+ _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
+ _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
+ }
+ }
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
index 2ee5b4e88..3f3a0dec5 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
@@ -1,7 +1,6 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using Jellyfin.Data.Events;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Authentication;
using MediaBrowser.Model.Activity;
diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
index 9a473de52..9626817e9 100644
--- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
@@ -1,5 +1,4 @@
-using Jellyfin.Data.Events;
-using Jellyfin.Data.Events.System;
+using Jellyfin.Data.Events.System;
using Jellyfin.Data.Events.Users;
using Jellyfin.Server.Implementations.Events.Consumers.Library;
using Jellyfin.Server.Implementations.Events.Consumers.Security;
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 390ed58b3..df1d5a3e1 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -6,8 +6,12 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
- <!-- Code analysers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index 0d91707e3..ea99af004 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
/// </summary>
public DbSet<User> Users => Set<User>();
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata.
+ /// </summary>
+ public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>();
+
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
new file mode 100644
index 000000000..28baf1992
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
@@ -0,0 +1,681 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230626233818_AddTrickplayInfos")]
+ partial class AddTrickplayInfos
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
new file mode 100644
index 000000000..76b12de08
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
@@ -0,0 +1,40 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddTrickplayInfos : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "TrickplayInfos",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ Width = table.Column<int>(type: "INTEGER", nullable: false),
+ Height = table.Column<int>(type: "INTEGER", nullable: false),
+ TileWidth = table.Column<int>(type: "INTEGER", nullable: false),
+ TileHeight = table.Column<int>(type: "INTEGER", nullable: false),
+ ThumbnailCount = table.Column<int>(type: "INTEGER", nullable: false),
+ Interval = table.Column<int>(type: "INTEGER", nullable: false),
+ Bandwidth = table.Column<int>(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width });
+ });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "TrickplayInfos");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
new file mode 100644
index 000000000..2884d4256
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
@@ -0,0 +1,654 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230923170422_UserCastReceiver")]
+ partial class UserCastReceiver
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
new file mode 100644
index 000000000..f06410c15
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class UserCastReceiver : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "CastReceiverId",
+ table: "Users",
+ type: "TEXT",
+ maxLength: 32,
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CastReceiverId",
+ table: "Users");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index d23508096..f725ababe 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -1,4 +1,4 @@
-// <auto-generated />
+// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("DeviceOptions");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
@@ -457,6 +488,10 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasMaxLength(255)
.HasColumnType("TEXT");
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
new file mode 100644
index 000000000..dc1c17e5e
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the TrickplayInfo entity.
+ /// </summary>
+ public class TrickplayInfoConfiguration : IEntityTypeConfiguration<TrickplayInfo>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<TrickplayInfo> builder)
+ {
+ builder.HasKey(info => new { info.ItemId, info.Width });
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 700e63970..77f8f7071 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
/// <summary>
/// Gets the authorization.
/// </summary>
- /// <param name="httpReq">The HTTP req.</param>
+ /// <param name="httpContext">The HTTP context.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
+ private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
{
- var auth = GetAuthorizationDictionary(httpReq);
- var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
+ var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
- httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
+ httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
@@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
auth.TryGetValue("Token", out token);
}
-#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
if (string.IsNullOrEmpty(token))
{
token = headers["X-Emby-Token"];
@@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
// Request doesn't contain a token.
return authInfo;
}
-#pragma warning restore CA1508
authInfo.HasToken = true;
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
@@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
/// <summary>
/// Gets the auth.
/// </summary>
- /// <param name="httpReq">The HTTP req.</param>
- /// <returns>Dictionary{System.StringSystem.String}.</returns>
- private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
- {
- var auth = httpReq.Request.Headers["X-Emby-Authorization"];
-
- if (string.IsNullOrEmpty(auth))
- {
- auth = httpReq.Request.Headers[HeaderNames.Authorization];
- }
-
- return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
- }
-
- /// <summary>
- /// Gets the auth.
- /// </summary>
- /// <param name="httpReq">The HTTP req.</param>
+ /// <param name="httpReq">The HTTP request.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
new file mode 100644
index 000000000..b960feb7f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -0,0 +1,474 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Trickplay;
+
+/// <summary>
+/// ITrickplayManager implementation.
+/// </summary>
+public class TrickplayManager : ITrickplayManager
+{
+ private readonly ILogger<TrickplayManager> _logger;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+ private readonly IImageEncoder _imageEncoder;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IApplicationPaths _appPaths;
+
+ private static readonly SemaphoreSlim _resourcePool = new(1, 1);
+ private static readonly string[] _trickplayImgExtensions = { ".jpg" };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayManager"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="fileSystem">The file systen.</param>
+ /// <param name="encodingHelper">The encoding helper.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="config">The server configuration manager.</param>
+ /// <param name="imageEncoder">The image encoder.</param>
+ /// <param name="dbProvider">The database provider.</param>
+ /// <param name="appPaths">The application paths.</param>
+ public TrickplayManager(
+ ILogger<TrickplayManager> logger,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ EncodingHelper encodingHelper,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager config,
+ IImageEncoder imageEncoder,
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IApplicationPaths appPaths)
+ {
+ _logger = logger;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _encodingHelper = encodingHelper;
+ _libraryManager = libraryManager;
+ _config = config;
+ _imageEncoder = imageEncoder;
+ _dbProvider = dbProvider;
+ _appPaths = appPaths;
+ }
+
+ /// <inheritdoc />
+ public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
+
+ var options = _config.Configuration.TrickplayOptions;
+ foreach (var width in options.WidthResolutions)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await RefreshTrickplayDataInternal(
+ video,
+ replace,
+ width,
+ options,
+ cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task RefreshTrickplayDataInternal(
+ Video video,
+ bool replace,
+ int width,
+ TrickplayOptions options,
+ CancellationToken cancellationToken)
+ {
+ if (!CanGenerateTrickplay(video, options.Interval))
+ {
+ return;
+ }
+
+ var imgTempDir = string.Empty;
+ var outputDir = GetTrickplayDirectory(video, width);
+
+ await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
+ {
+ _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
+ return;
+ }
+
+ // Extract images
+ // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
+ var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
+
+ if (mediaSource is null)
+ {
+ _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
+ return;
+ }
+
+ var mediaPath = mediaSource.Path;
+ var mediaStream = mediaSource.VideoStream;
+ var container = mediaSource.Container;
+
+ _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
+ imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
+ mediaPath,
+ container,
+ mediaSource,
+ mediaStream,
+ width,
+ TimeSpan.FromMilliseconds(options.Interval),
+ options.EnableHwAcceleration,
+ options.ProcessThreads,
+ options.Qscale,
+ options.ProcessPriority,
+ _encodingHelper,
+ cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
+ {
+ throw new InvalidOperationException("Null or invalid directory from media encoder.");
+ }
+
+ var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
+ .Select(i => i.FullName)
+ .OrderBy(i => i)
+ .ToList();
+
+ // Create tiles
+ var trickplayInfo = CreateTiles(images, width, options, outputDir);
+
+ // Save tiles info
+ try
+ {
+ if (trickplayInfo is not null)
+ {
+ trickplayInfo.ItemId = video.Id;
+ await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
+
+ _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+ }
+ else
+ {
+ throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while saving trickplay tiles info.");
+
+ // Make sure no files stay in metadata folders on failure
+ // if tiles info wasn't saved.
+ Directory.Delete(outputDir, true);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating trickplay images.");
+ }
+ finally
+ {
+ _resourcePool.Release();
+
+ if (!string.IsNullOrEmpty(imgTempDir))
+ {
+ Directory.Delete(imgTempDir, true);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir)
+ {
+ if (images.Count == 0)
+ {
+ throw new ArgumentException("Can't create trickplay from 0 images.");
+ }
+
+ var workDir = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(workDir);
+
+ var trickplayInfo = new TrickplayInfo
+ {
+ Width = width,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
+ ThumbnailCount = images.Count,
+ // Set during image generation
+ Height = 0,
+ Bandwidth = 0
+ };
+
+ /*
+ * Generate trickplay tiles from sets of thumbnails
+ */
+ var imageOptions = new ImageCollageOptions
+ {
+ Width = trickplayInfo.TileWidth,
+ Height = trickplayInfo.TileHeight
+ };
+
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
+
+ for (int i = 0; i < requiredTiles; i++)
+ {
+ // Set output/input paths
+ var tilePath = Path.Combine(workDir, $"{i}.jpg");
+
+ imageOptions.OutputPath = tilePath;
+ imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile)));
+
+ // Generate image and use returned height for tiles info
+ var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
+ if (trickplayInfo.Height == 0)
+ {
+ trickplayInfo.Height = height;
+ }
+
+ // Update bitrate
+ var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
+ trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
+ }
+
+ /*
+ * Move trickplay tiles to output directory
+ */
+ Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
+
+ // Replace existing tiles if they already exist
+ if (Directory.Exists(outputDir))
+ {
+ Directory.Delete(outputDir, true);
+ }
+
+ MoveDirectory(workDir, outputDir);
+
+ return trickplayInfo;
+ }
+
+ private bool CanGenerateTrickplay(Video video, int interval)
+ {
+ var videoType = video.VideoType;
+ if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
+ {
+ return false;
+ }
+
+ if (video.IsPlaceHolder)
+ {
+ return false;
+ }
+
+ if (video.IsShortcut)
+ {
+ return false;
+ }
+
+ if (!video.IsCompleteMedia)
+ {
+ return false;
+ }
+
+ if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
+ {
+ return false;
+ }
+
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction)
+ {
+ return false;
+ }
+
+ // Can't extract images if there are no video streams
+ return video.GetMediaStreams().Count > 0;
+ }
+
+ /// <inheritdoc />
+ public async Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId)
+ {
+ var trickplayResolutions = new Dictionary<int, TrickplayInfo>();
+
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var trickplayInfos = await dbContext.TrickplayInfos
+ .AsNoTracking()
+ .Where(i => i.ItemId.Equals(itemId))
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ foreach (var info in trickplayInfos)
+ {
+ trickplayResolutions[info.Width] = info;
+ }
+ }
+
+ return trickplayResolutions;
+ }
+
+ /// <inheritdoc />
+ public async Task SaveTrickplayInfo(TrickplayInfo info)
+ {
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false);
+ if (oldInfo is not null)
+ {
+ dbContext.TrickplayInfos.Remove(oldInfo);
+ }
+
+ dbContext.Add(info);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+
+ /// <inheritdoc />
+ public async Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item)
+ {
+ var trickplayManifest = new Dictionary<string, Dictionary<int, TrickplayInfo>>();
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var mediaSourceId = Guid.Parse(mediaSource.Id);
+ var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false);
+
+ if (trickplayResolutions.Count > 0)
+ {
+ trickplayManifest[mediaSource.Id] = trickplayResolutions;
+ }
+ }
+
+ return trickplayManifest;
+ }
+
+ /// <inheritdoc />
+ public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ {
+ return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+ }
+
+ /// <inheritdoc />
+ public async Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey)
+ {
+ var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false);
+ if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
+ {
+ var builder = new StringBuilder(128);
+
+ if (trickplayInfo.ThumbnailCount > 0)
+ {
+ const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
+ const string decimalFormat = "{0:0.###}";
+
+ var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
+ var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}";
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var thumbnailDuration = trickplayInfo.Interval / 1000d;
+ var infDuration = thumbnailDuration * thumbnailsPerTile;
+ var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile);
+
+ builder
+ .AppendLine("#EXTM3U")
+ .Append("#EXT-X-TARGETDURATION:")
+ .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture))
+ .AppendLine("#EXT-X-VERSION:7")
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+ .AppendLine("#EXT-X-IMAGES-ONLY");
+
+ for (int i = 0; i < tileCount; i++)
+ {
+ // All tiles prior to the last must contain full amount of thumbnails (no black).
+ if (i == tileCount - 1)
+ {
+ thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile);
+ infDuration = thumbnailDuration * thumbnailsPerTile;
+ }
+
+ // EXTINF
+ builder
+ .Append("#EXTINF:")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
+ .AppendLine(",");
+
+ // EXT-X-TILES
+ builder
+ .Append("#EXT-X-TILES:RESOLUTION=")
+ .Append(resolution)
+ .Append(",LAYOUT=")
+ .Append(layout)
+ .Append(",DURATION=")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration)
+ .AppendLine();
+
+ // URL
+ builder
+ .AppendFormat(
+ CultureInfo.InvariantCulture,
+ urlFormat,
+ width.ToString(CultureInfo.InvariantCulture),
+ i.ToString(CultureInfo.InvariantCulture),
+ itemId.ToString("N"),
+ apiKey)
+ .AppendLine();
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return builder.ToString();
+ }
+ }
+
+ return null;
+ }
+
+ private string GetTrickplayDirectory(BaseItem item, int? width = null)
+ {
+ var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ }
+
+ private void MoveDirectory(string source, string destination)
+ {
+ try
+ {
+ Directory.Move(source, destination);
+ }
+ catch (IOException)
+ {
+ // Cross device move requires a copy
+ Directory.CreateDirectory(destination);
+ foreach (string file in Directory.GetFiles(source))
+ {
+ File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
+ }
+
+ Directory.Delete(source, true);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
index 72f3d6e8e..cb2d09a67 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Authentication;
@@ -39,14 +40,18 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc />
// This is the version that we need to use for local users. Because reasons.
- public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
+ public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser)
{
- if (resolvedUser is null)
+ [DoesNotReturn]
+ static void ThrowAuthenticationException()
{
- throw new AuthenticationException("Specified user does not exist.");
+ throw new AuthenticationException("Invalid username or password");
}
- bool success = false;
+ if (resolvedUser is null)
+ {
+ ThrowAuthenticationException();
+ }
// As long as jellyfin supports password-less users, we need this little block here to accommodate
if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
@@ -60,15 +65,13 @@ namespace Jellyfin.Server.Implementations.Users
// Handle the case when the stored password is null, but the user tried to login with a password
if (resolvedUser.Password is null)
{
- throw new AuthenticationException("Invalid username or password");
+ ThrowAuthenticationException();
}
PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
- success = _cryptographyProvider.Verify(readyHash, password);
-
- if (!success)
+ if (!_cryptographyProvider.Verify(readyHash, password))
{
- throw new AuthenticationException("Invalid username or password");
+ ThrowAuthenticationException();
}
// Migrate old hashes to the new default
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index bfae81e4c..edc6aa173 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller;
using Microsoft.EntityFrameworkCore;
@@ -13,7 +14,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <summary>
/// Manages the storage and retrieval of display preferences through Entity Framework.
/// </summary>
- public class DisplayPreferencesManager : IDisplayPreferencesManager
+ public sealed class DisplayPreferencesManager : IDisplayPreferencesManager, IAsyncDisposable
{
private readonly JellyfinDbContext _dbContext;
@@ -97,5 +98,11 @@ namespace Jellyfin.Server.Implementations.Users
{
_dbContext.SaveChanges();
}
+
+ /// <inheritdoc />
+ public async ValueTask DisposeAsync()
+ {
+ await _dbContext.DisposeAsync().ConfigureAwait(false);
+ }
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index ec0c64cd7..edae4cfc5 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -15,12 +15,12 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Users;
using Microsoft.EntityFrameworkCore;
@@ -35,7 +35,6 @@ namespace Jellyfin.Server.Implementations.Users
{
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IEventManager _eventManager;
- private readonly ICryptoProvider _cryptoProvider;
private readonly INetworkManager _networkManager;
private readonly IApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
@@ -45,6 +44,7 @@ namespace Jellyfin.Server.Implementations.Users
private readonly InvalidAuthProvider _invalidAuthProvider;
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users;
@@ -53,27 +53,27 @@ namespace Jellyfin.Server.Implementations.Users
/// </summary>
/// <param name="dbProvider">The database provider.</param>
/// <param name="eventManager">The event manager.</param>
- /// <param name="cryptoProvider">The cryptography provider.</param>
/// <param name="networkManager">The network manager.</param>
/// <param name="appHost">The application host.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
+ /// <param name="serverConfigurationManager">The system config manager.</param>
public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager,
- ICryptoProvider cryptoProvider,
INetworkManager networkManager,
IApplicationHost appHost,
IImageProcessor imageProcessor,
- ILogger<UserManager> logger)
+ ILogger<UserManager> logger,
+ IServerConfigurationManager serverConfigurationManager)
{
_dbProvider = dbProvider;
_eventManager = eventManager;
- _cryptoProvider = cryptoProvider;
_networkManager = networkManager;
_appHost = appHost;
_imageProcessor = imageProcessor;
_logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
@@ -108,7 +108,7 @@ namespace Jellyfin.Server.Implementations.Users
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
- [GeneratedRegex("^[\\w\\ \\-'._@]+$")]
+ [GeneratedRegex(@"^[\w\ \-'._@]+$")]
private static partial Regex ValidUsernameRegex();
/// <inheritdoc/>
@@ -293,6 +293,7 @@ namespace Jellyfin.Server.Implementations.Users
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
{
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
+ var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
return new UserDto
{
Name = user.Username,
@@ -320,7 +321,11 @@ namespace Jellyfin.Server.Implementations.Users
OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
- LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
+ LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
+ CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
+ ? castReceiverApplications.FirstOrDefault()?.Id
+ : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringComparison.Ordinal))?.Id
+ ?? castReceiverApplications.FirstOrDefault()?.Id
},
Policy = new UserPolicy
{
@@ -354,6 +359,7 @@ namespace Jellyfin.Server.Implementations.Users
ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
+ EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
@@ -384,7 +390,7 @@ namespace Jellyfin.Server.Implementations.Users
}
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
- var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
+ var authResult = await AuthenticateLocalUser(username, password, user)
.ConfigureAwait(false);
var authenticationProvider = authResult.AuthenticationProvider;
var success = authResult.Success;
@@ -609,6 +615,13 @@ namespace Jellyfin.Server.Implementations.Users
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+ // Only set cast receiver id if it is passed in and it exists in the server config.
+ if (!string.IsNullOrEmpty(config.CastReceiverId)
+ && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal)))
+ {
+ user.CastReceiverId = config.CastReceiverId;
+ }
+
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
@@ -671,6 +684,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
+ user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
@@ -787,8 +801,7 @@ namespace Jellyfin.Server.Implementations.Users
private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser(
string username,
string password,
- User? user,
- string remoteEndPoint)
+ User? user)
{
bool success = false;
IAuthenticationProvider? authenticationProvider = null;
@@ -833,7 +846,7 @@ namespace Jellyfin.Server.Implementations.Users
}
catch (AuthenticationException ex)
{
- _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
+ _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
return (username, false);
}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 0c6315c66..c12c90a68 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Security;
+using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
@@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Activity;
using MediaBrowser.Providers.Lyric;
using Microsoft.Extensions.Configuration;
@@ -78,6 +80,7 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+ serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
// TODO search the assemblies instead of adding them manually?
serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
@@ -103,9 +106,6 @@ namespace Jellyfin.Server
}
/// <inheritdoc />
- protected override void RestartInternal() => Program.Restart();
-
- /// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
// Jellyfin.Server
@@ -114,8 +114,5 @@ namespace Jellyfin.Server
// Jellyfin.Server.Implementations
yield return typeof(JellyfinDbContext).Assembly;
}
-
- /// <inheritdoc />
- protected override void ShutdownInternal() => Program.Shutdown();
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index e1dfa1d31..b7e71a81d 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options =>
{
@@ -81,6 +82,7 @@ namespace Jellyfin.Server.Extensions
options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
+ options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement));
options.AddPolicy(
Policies.RequiresElevation,
policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
@@ -276,12 +278,12 @@ namespace Jellyfin.Server.Extensions
}
else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet))
{
- if (subnet != null)
+ if (subnet is not null)
{
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
}
}
- else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses))
+ else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
{
foreach (var address in addresses)
{
diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
index 3cb791b57..c9d5b54de 100644
--- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
@@ -3,7 +3,6 @@ using System.IO;
using System.Net;
using Jellyfin.Server.Helpers;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
@@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions
return builder
.UseKestrel((builderContext, options) =>
{
- var addresses = appHost.NetManager.GetAllBindInterfaces();
+ var addresses = appHost.NetManager.GetAllBindInterfaces(true);
bool flagged = false;
foreach (var netAdd in addresses)
diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs
index fda6e5465..66d393dec 100644
--- a/Jellyfin.Server/Helpers/StartupHelpers.cs
+++ b/Jellyfin.Server/Helpers/StartupHelpers.cs
@@ -15,7 +15,6 @@ using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Serilog;
-using SQLitePCL;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server.Helpers;
@@ -297,7 +296,5 @@ public static class StartupHelpers
// Disable the "Expect: 100-Continue" header by default
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
ServicePointManager.Expect100Continue = false;
-
- Batteries_V2.Init();
}
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 146de3ae1..5479d2296 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -22,8 +22,12 @@
<EmbeddedResource Include="Resources/Configuration/*" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@@ -48,7 +52,6 @@
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Serilog.Sinks.Graylog" />
- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 2db0b77cd..757b56a49 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -42,7 +42,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDownloadImagesInAdvance),
typeof(Routines.MigrateAuthenticationDb),
typeof(Routines.FixPlaylistOwner),
- typeof(Routines.MigrateRatingLevels)
+ typeof(Routines.MigrateRatingLevels),
+ typeof(Routines.AddDefaultCastReceivers)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
index bee135efd..0544fe561 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
@@ -59,21 +59,17 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
private OldMusicBrainzConfiguration? ReadOld(string path)
{
- using (var xmlReader = XmlReader.Create(path))
- {
- var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
- return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
- }
+ using var xmlReader = XmlReader.Create(path);
+ var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
+ return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
}
private void WriteNew(string path, PluginConfiguration newPluginConfiguration)
{
var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration"));
var xmlWriterSettings = new XmlWriterSettings { Indent = true };
- using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings))
- {
- pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration);
- }
+ using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
+ pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration);
}
#pragma warning disable
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
index a4379197c..c6d86b8cd 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
@@ -43,10 +43,8 @@ public class MigrateNetworkConfiguration : IMigrationRoutine
try
{
- using (var xmlReader = XmlReader.Create(path))
- {
- oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader);
- }
+ using var xmlReader = XmlReader.Create(path);
+ oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader);
}
catch (InvalidOperationException ex)
{
@@ -97,10 +95,8 @@ public class MigrateNetworkConfiguration : IMigrationRoutine
var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration));
var xmlWriterSettings = new XmlWriterSettings { Indent = true };
- using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings))
- {
- networkConfigSerializer.Serialize(xmlWriter, networkConfiguration);
- }
+ using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
+ networkConfigSerializer.Serialize(xmlWriter, networkConfiguration);
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
new file mode 100644
index 000000000..75a6a6176
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
@@ -0,0 +1,55 @@
+using System;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to add the default cast receivers to the system config.
+/// </summary>
+public class AddDefaultCastReceivers : IMigrationRoutine
+{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AddDefaultCastReceivers"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public AddDefaultCastReceivers(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc />
+ public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8");
+
+ /// <inheritdoc />
+ public string Name => "AddDefaultCastReceivers";
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => true;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ // Only add if receiver list is empty.
+ if (_serverConfigurationManager.Configuration.CastReceiverApplications.Length == 0)
+ {
+ _serverConfigurationManager.Configuration.CastReceiverApplications = new CastReceiverApplication[]
+ {
+ new()
+ {
+ Id = "F007D354",
+ Name = "Stable"
+ },
+ new()
+ {
+ Id = "6F511C87",
+ Name = "Unstable"
+ }
+ };
+
+ _serverConfigurationManager.SaveConfiguration();
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index e8a0af9f8..2f23cb1f8 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -5,9 +5,9 @@ using Emby.Server.Implementations.Data;
using Jellyfin.Data.Entities;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
+using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines
{
@@ -61,17 +61,15 @@ namespace Jellyfin.Server.Migrations.Routines
};
var dataPath = _paths.DataPath;
- using (var connection = SQLite3.Open(
- Path.Combine(dataPath, DbFilename),
- ConnectionFlags.ReadOnly,
- null))
+ using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
{
- using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null);
+ connection.Open();
+
+ using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
+ userDbConnection.Open();
_logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
using var dbContext = _provider.CreateDbContext();
- var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
-
// Make sure that the database is empty in case of failed migration due to power outages, etc.
dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs);
dbContext.SaveChanges();
@@ -81,51 +79,52 @@ namespace Jellyfin.Server.Migrations.Routines
var newEntries = new List<ActivityLog>();
+ var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
+
foreach (var entry in queryResult)
{
- if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity))
+ if (!logLevelDictionary.TryGetValue(entry.GetString(8), out var severity))
{
severity = LogLevel.Trace;
}
var guid = Guid.Empty;
- if (entry[6].SQLiteType != SQLiteType.Null && !Guid.TryParse(entry[6].ToString(), out guid))
+ if (!entry.IsDBNull(6) && !entry.TryGetGuid(6, out guid))
{
+ var id = entry.GetString(6);
// This is not a valid Guid, see if it is an internal ID from an old Emby schema
- _logger.LogWarning("Invalid Guid in UserId column: {Guid}", entry[6].ToString());
+ _logger.LogWarning("Invalid Guid in UserId column: {Guid}", id);
using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id");
- statement.TryBind("@Id", entry[6].ToString());
+ statement.TryBind("@Id", id);
- foreach (var row in statement.Query())
+ using var reader = statement.ExecuteReader();
+ if (reader.HasRows && reader.Read() && reader.TryGetGuid(0, out guid))
{
- if (row.Count > 0 && Guid.TryParse(row[0].ToString(), out guid))
- {
- // Successfully parsed a Guid from the user table.
- break;
- }
+ // Successfully parsed a Guid from the user table.
+ break;
}
}
- var newEntry = new ActivityLog(entry[1].ToString(), entry[4].ToString(), guid)
+ var newEntry = new ActivityLog(entry.GetString(1), entry.GetString(4), guid)
{
- DateCreated = entry[7].ReadDateTime(),
+ DateCreated = entry.GetDateTime(7),
LogSeverity = severity
};
- if (entry[2].SQLiteType != SQLiteType.Null)
+ if (entry.TryGetString(2, out var result))
{
- newEntry.Overview = entry[2].ToString();
+ newEntry.Overview = result;
}
- if (entry[3].SQLiteType != SQLiteType.Null)
+ if (entry.TryGetString(3, out result))
{
- newEntry.ShortOverview = entry[3].ToString();
+ newEntry.ShortOverview = result;
}
- if (entry[5].SQLiteType != SQLiteType.Null)
+ if (entry.TryGetString(5, out result))
{
- newEntry.ItemId = entry[5].ToString();
+ newEntry.ItemId = result;
}
newEntries.Add(newEntry);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
index 09daae0ff..c845beef2 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -6,9 +6,9 @@ using Jellyfin.Data.Entities.Security;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
+using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines
{
@@ -56,34 +56,32 @@ namespace Jellyfin.Server.Migrations.Routines
public void Perform()
{
var dataPath = _appPaths.DataPath;
- using (var connection = SQLite3.Open(
- Path.Combine(dataPath, DbFilename),
- ConnectionFlags.ReadOnly,
- null))
+ using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
{
+ connection.Open();
using var dbContext = _dbProvider.CreateDbContext();
var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
foreach (var row in authenticatedDevices)
{
- var dateCreatedStr = row[9].ToString();
+ var dateCreatedStr = row.GetString(9);
_ = DateTime.TryParse(dateCreatedStr, out var dateCreated);
- var dateLastActivityStr = row[10].ToString();
+ var dateLastActivityStr = row.GetString(10);
_ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity);
- if (row[6].IsDbNull())
+ if (row.IsDBNull(6))
{
- dbContext.ApiKeys.Add(new ApiKey(row[3].ToString())
+ dbContext.ApiKeys.Add(new ApiKey(row.GetString(3))
{
- AccessToken = row[1].ToString(),
+ AccessToken = row.GetString(1),
DateCreated = dateCreated,
DateLastActivity = dateLastActivity
});
}
else
{
- var userId = new Guid(row[6].ToString());
+ var userId = row.GetGuid(6);
var user = _userManager.GetUserById(userId);
if (user is null)
{
@@ -92,14 +90,14 @@ namespace Jellyfin.Server.Migrations.Routines
}
dbContext.Devices.Add(new Device(
- new Guid(row[6].ToString()),
- row[3].ToString(),
- row[4].ToString(),
- row[5].ToString(),
- row[2].ToString())
+ userId,
+ row.GetString(3),
+ row.GetString(4),
+ row.GetString(5),
+ row.GetString(2))
{
- AccessToken = row[1].ToString(),
- IsActive = row[8].ToBool(),
+ AccessToken = row.GetString(1),
+ IsActive = row.GetBoolean(8),
DateCreated = dateCreated,
DateLastActivity = dateLastActivity
});
@@ -110,12 +108,12 @@ namespace Jellyfin.Server.Migrations.Routines
var deviceIds = new HashSet<string>();
foreach (var row in deviceOptions)
{
- if (row[2].IsDbNull())
+ if (row.IsDBNull(2))
{
continue;
}
- var deviceId = row[2].ToString();
+ var deviceId = row.GetString(2);
if (deviceIds.Contains(deviceId))
{
continue;
@@ -125,7 +123,7 @@ namespace Jellyfin.Server.Migrations.Routines
dbContext.DeviceOptions.Add(new DeviceOptions(deviceId)
{
- CustomName = row[1].IsDbNull() ? null : row[1].ToString()
+ CustomName = row.IsDBNull(1) ? null : row.GetString(1)
});
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 8fe2b087d..249b39ae4 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -4,15 +4,16 @@ using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
+using Emby.Server.Implementations.Data;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines
{
@@ -83,22 +84,23 @@ namespace Jellyfin.Server.Migrations.Routines
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
- using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
+ using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
{
+ connection.Open();
using var dbContext = _provider.CreateDbContext();
var results = connection.Query("SELECT * FROM userdisplaypreferences");
foreach (var result in results)
{
- var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToBlob(), _jsonOptions);
+ var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result.GetStream(3), _jsonOptions);
if (dto is null)
{
continue;
}
- var itemId = new Guid(result[1].ToBlob());
- var dtoUserId = new Guid(result[1].ToBlob());
- var client = result[2].ToString();
+ var itemId = result.GetGuid(1);
+ var dtoUserId = itemId;
+ var client = result.GetString(2);
var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}";
if (displayPrefs.Contains(displayPreferencesKey))
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 9dee520a5..ac5047401 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -1,13 +1,11 @@
using System;
using System.Globalization;
using System.IO;
-
using Emby.Server.Implementations.Data;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Globalization;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines
{
@@ -20,17 +18,14 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger<MigrateRatingLevels> _logger;
private readonly IServerApplicationPaths _applicationPaths;
private readonly ILocalizationManager _localizationManager;
- private readonly IItemRepository _repository;
public MigrateRatingLevels(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
- ILocalizationManager localizationManager,
- IItemRepository repository)
+ ILocalizationManager localizationManager)
{
_applicationPaths = applicationPaths;
_localizationManager = localizationManager;
- _repository = repository;
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
}
@@ -70,16 +65,14 @@ namespace Jellyfin.Server.Migrations.Routines
// Migrate parental rating strings to new levels
_logger.LogInformation("Recalculating parental rating levels based on rating string.");
- using (var connection = SQLite3.Open(
- dbPath,
- ConnectionFlags.ReadWrite,
- null))
+ using var connection = new SqliteConnection($"Filename={dbPath}");
+ connection.Open();
+ using (var transaction = connection.BeginTransaction())
{
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
foreach (var entry in queryResult)
{
- var ratingString = entry[0].ToString();
- if (string.IsNullOrEmpty(ratingString))
+ if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString))
{
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
}
@@ -91,12 +84,14 @@ namespace Jellyfin.Server.Migrations.Routines
ratingValue = "NULL";
}
- var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
+ using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
statement.TryBind("@Value", ratingValue);
statement.TryBind("@Rating", ratingString);
- statement.ExecuteQuery();
+ statement.ExecuteNonQuery();
}
}
+
+ transaction.Commit();
}
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 0186500a1..4fee88b68 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -11,9 +11,9 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
+using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Jellyfin.Server.Migrations.Routines
@@ -64,8 +64,9 @@ namespace Jellyfin.Server.Migrations.Routines
var dataPath = _paths.DataPath;
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
- using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
+ using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
{
+ connection.Open();
var dbContext = _provider.CreateDbContext();
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
@@ -75,7 +76,7 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var entry in queryResult)
{
- UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.Options);
+ UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options);
if (mockup is null)
{
continue;
@@ -108,8 +109,8 @@ namespace Jellyfin.Server.Migrations.Routines
var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
{
- Id = entry[1].ReadGuidFromBlob(),
- InternalId = entry[0].ToInt64(),
+ Id = entry.GetGuid(1),
+ InternalId = entry.GetInt64(0),
MaxParentalAgeRating = policy.MaxParentalRating,
EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
index 6c26e47e1..7b0d9456d 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
@@ -1,10 +1,11 @@
using System;
using System.Globalization;
using System.IO;
-
+using System.Linq;
+using Emby.Server.Implementations.Data;
using MediaBrowser.Controller;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines
{
@@ -37,14 +38,13 @@ namespace Jellyfin.Server.Migrations.Routines
{
var dataPath = _paths.DataPath;
var dbPath = Path.Combine(dataPath, DbFilename);
- using (var connection = SQLite3.Open(
- dbPath,
- ConnectionFlags.ReadWrite,
- null))
+ using var connection = new SqliteConnection($"Filename={dbPath}");
+ connection.Open();
+ using (var transaction = connection.BeginTransaction())
{
// Query the database for the ids of duplicate extras
var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
- var bads = string.Join(", ", queryResult.SelectScalarString());
+ var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)));
// Do nothing if no duplicate extras were detected
if (bads.Length == 0)
@@ -76,6 +76,7 @@ namespace Jellyfin.Server.Migrations.Routines
// Delete all duplicate extras
_logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
+ transaction.Commit();
}
}
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 6e8b17a73..f9259d0d9 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
@@ -42,7 +41,6 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json";
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
- private static CancellationTokenSource _tokenSource = new();
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@@ -65,36 +63,9 @@ namespace Jellyfin.Server
.MapResult(StartApp, ErrorParsingArguments);
}
- /// <summary>
- /// Shuts down the application.
- /// </summary>
- internal static void Shutdown()
- {
- if (!_tokenSource.IsCancellationRequested)
- {
- _tokenSource.Cancel();
- }
- }
-
- /// <summary>
- /// Restarts the application.
- /// </summary>
- internal static void Restart()
- {
- _restartOnShutdown = true;
-
- Shutdown();
- }
-
private static async Task StartApp(StartupOptions options)
{
_startTimestamp = Stopwatch.GetTimestamp();
-
- // Log all uncaught exceptions to std error
- static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
- Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject);
- AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
-
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
@@ -112,38 +83,10 @@ namespace Jellyfin.Server
StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
- // Log uncaught exceptions to the logging instead of std error
- AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
+ // Use the logging framework for uncaught exceptions instead of std error
AppDomain.CurrentDomain.UnhandledException += (_, e)
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
- // Intercept Ctrl+C and Ctrl+Break
- Console.CancelKeyPress += (_, e) =>
- {
- if (_tokenSource.IsCancellationRequested)
- {
- return; // Already shutting down
- }
-
- e.Cancel = true;
- _logger.LogInformation("Ctrl+C, shutting down");
- Environment.ExitCode = 128 + 2;
- Shutdown();
- };
-
- // Register a SIGTERM handler
- AppDomain.CurrentDomain.ProcessExit += (_, _) =>
- {
- if (_tokenSource.IsCancellationRequested)
- {
- return; // Already shutting down
- }
-
- _logger.LogInformation("Received a SIGTERM signal, shutting down");
- Environment.ExitCode = 128 + 15;
- Shutdown();
- };
-
_logger.LogInformation(
"Jellyfin version: {Version}",
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
@@ -173,12 +116,10 @@ namespace Jellyfin.Server
do
{
- _restartOnShutdown = false;
await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
if (_restartOnShutdown)
{
- _tokenSource = new CancellationTokenSource();
_startTimestamp = Stopwatch.GetTimestamp();
}
} while (_restartOnShutdown);
@@ -186,7 +127,7 @@ namespace Jellyfin.Server
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{
- var appHost = new CoreAppHost(
+ using var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
@@ -196,6 +137,7 @@ namespace Jellyfin.Server
try
{
host = Host.CreateDefaultBuilder()
+ .UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
@@ -210,7 +152,7 @@ namespace Jellyfin.Server
try
{
- await host.StartAsync(_tokenSource.Token).ConfigureAwait(false);
+ await host.StartAsync().ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
@@ -219,22 +161,18 @@ namespace Jellyfin.Server
StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
}
}
- catch (Exception ex) when (ex is not TaskCanceledException)
+ catch (Exception)
{
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again");
throw;
}
- await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
+ await appHost.RunStartupTasksAsync().ConfigureAwait(false);
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
- // Block main thread until shutdown
- await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
- }
- catch (TaskCanceledException)
- {
- // Don't throw on cancellation
+ await host.WaitForShutdownAsync().ConfigureAwait(false);
+ _restartOnShutdown = appHost.ShouldRestart;
}
catch (Exception ex)
{
@@ -257,7 +195,6 @@ namespace Jellyfin.Server
}
}
- await appHost.DisposeAsync().ConfigureAwait(false);
host?.Dispose();
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index b759b6bca..2acddb243 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,10 +1,10 @@
using System;
-using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
+using Emby.Dlna.Extensions;
using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking.Configuration;
@@ -27,7 +27,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
-using Microsoft.VisualBasic;
using Prometheus;
namespace Jellyfin.Server
@@ -120,26 +119,11 @@ namespace Jellyfin.Server
})
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
- services.AddHttpClient(NamedClient.Dlna, c =>
- {
- c.DefaultRequestHeaders.UserAgent.ParseAdd(
- string.Format(
- CultureInfo.InvariantCulture,
- "{0}/{1} UPnP/1.0 {2}/{3}",
- Environment.OSVersion.Platform,
- Environment.OSVersion,
- _serverApplicationHost.Name,
- _serverApplicationHost.ApplicationVersionString));
-
- c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", _serverApplicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
- c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", _serverApplicationHost.FriendlyName); // REVIEW: where does this come from?
- })
- .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
-
services.AddHealthChecks()
.AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
services.AddHlsPlaylistGenerator();
+ services.AddDlnaServices(_serverApplicationHost);
}
/// <summary>
diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
index c3a7cb394..bb8ab130d 100644
--- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs
+++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
@@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions
/// </summary>
/// <param name="process">The process to wait for.</param>
/// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
- /// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns>
- /// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception>
- public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
+ /// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns>
+ /// <exception cref="OperationCanceledException">The timeout ended.</exception>
+ public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
{
using (var cancelTokenSource = new CancellationTokenSource(timeout))
{
- return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Asynchronously wait for the process to exit.
- /// </summary>
- /// <param name="process">The process to wait for.</param>
- /// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param>
- /// <returns>True if the task exited normally, false if cancelled before the process exited.</returns>
- public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken)
- {
- if (!process.EnableRaisingEvents)
- {
- throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit.");
- }
-
- // Add an event handler for the process exit event
- var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
- process.Exited += (_, _) => tcs.TrySetResult(true);
-
- // Return immediately if the process has already exited
- if (process.HasExitedSafe())
- {
- return true;
- }
-
- // Register with the cancellation token then await
- using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe())))
- {
- return await tcs.Task.ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Gets a value indicating whether the associated process has been terminated using
- /// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process
- /// associated with the <see cref="Process"/>.
- /// </summary>
- /// <param name="process">The process to check the exit status for.</param>
- /// <returns>
- /// True if the operating system process referenced by the <see cref="Process"/> component has
- /// terminated, or if there is no associated operating system process; otherwise, false.
- /// </returns>
- private static bool HasExitedSafe(this Process process)
- {
- try
- {
- return process.HasExited;
- }
- catch (InvalidOperationException)
- {
- return true;
+ await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false);
}
}
}
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 96ee701b3..23795c6be 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
-using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
@@ -36,16 +35,15 @@ namespace MediaBrowser.Common
string SystemId { get; }
/// <summary>
- /// Gets a value indicating whether this instance has pending kernel reload.
+ /// Gets a value indicating whether this instance has pending changes requiring a restart.
/// </summary>
- /// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
+ /// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
bool HasPendingRestart { get; }
/// <summary>
- /// Gets a value indicating whether this instance is currently shutting down.
+ /// Gets or sets a value indicating whether the application should restart.
/// </summary>
- /// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value>
- bool IsShuttingDown { get; }
+ bool ShouldRestart { get; set; }
/// <summary>
/// Gets the application version.
@@ -88,11 +86,6 @@ namespace MediaBrowser.Common
void NotifyPendingRestart();
/// <summary>
- /// Restarts this instance.
- /// </summary>
- void Restart();
-
- /// <summary>
/// Gets the exports.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
@@ -124,12 +117,6 @@ namespace MediaBrowser.Common
T Resolve<T>();
/// <summary>
- /// Shuts down.
- /// </summary>
- /// <returns>A task.</returns>
- Task Shutdown();
-
- /// <summary>
/// Initializes this instance.
/// </summary>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 3f1a098e4..7d0d7a173 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -38,17 +38,17 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
- </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>
- <!-- Code analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs
index 1d73de3c9..0ff9719e9 100644
--- a/MediaBrowser.Common/Plugins/IPluginManager.cs
+++ b/MediaBrowser.Common/Plugins/IPluginManager.cs
@@ -30,11 +30,6 @@ namespace MediaBrowser.Common.Plugins
IEnumerable<Assembly> LoadAssemblies();
/// <summary>
- /// Unloads all of the assemblies.
- /// </summary>
- void UnloadAssemblies();
-
- /// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
index a56d3c822..81b532fda 100644
--- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Threading.Tasks;
@@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Authentication
public interface IRequiresResolvedUser
{
- Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser);
+ Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser);
}
public interface IHasNewUserPolicy
@@ -33,8 +31,8 @@ namespace MediaBrowser.Controller.Authentication
public class ProviderAuthenticationResult
{
- public string Username { get; set; }
+ public required string Username { get; set; }
- public string DisplayName { get; set; }
+ public string? DisplayName { get; set; }
}
}
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index b263c173e..6acab13fe 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -1,5 +1,4 @@
using System;
-using System.Linq;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
index ac20120d9..975218ad7 100644
--- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
@@ -1,4 +1,3 @@
-using System.Threading;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
diff --git a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
index dea1c2f32..2a7e6be0f 100644
--- a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
+++ b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
@@ -23,9 +23,12 @@ namespace MediaBrowser.Controller.ClientEvent
{
var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName);
- await using var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
- await fileContents.CopyToAsync(fileStream).ConfigureAwait(false);
- return fileName;
+ var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ await using (fileStream.ConfigureAwait(false))
+ {
+ await fileContents.CopyToAsync(fileStream).ConfigureAwait(false);
+ return fileName;
+ }
}
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index e5c8ebfaf..c7bfbdb53 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -81,5 +81,15 @@ namespace MediaBrowser.Controller.Drawing
/// <param name="posters">The list of poster paths.</param>
/// <param name="backdrops">The list of backdrop paths.</param>
void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops);
+
+ /// <summary>
+ /// Creates a new trickplay tile image.
+ /// </summary>
+ /// <param name="options">The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels.</param>
+ /// <param name="quality">The image encode quality.</param>
+ /// <param name="imgWidth">The width of a single trickplay thumbnail.</param>
+ /// <param name="imgHeight">Optional height of a single trickplay thumbnail, if it is known.</param>
+ /// <returns>Height of single decoded trickplay thumbnail.</returns>
+ int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index e5ce0aa21..0d1e2a5a0 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
@@ -66,7 +65,7 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>Guid.</returns>
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
- string GetImageCacheTag(BaseItem item, ChapterInfo chapter);
+ string? GetImageCacheTag(BaseItem item, ChapterInfo chapter);
string? GetImageCacheTag(User user);
@@ -74,14 +73,6 @@ namespace MediaBrowser.Controller.Drawing
/// Processes the image.
/// </summary>
/// <param name="options">The options.</param>
- /// <param name="toStream">To stream.</param>
- /// <returns>Task.</returns>
- Task ProcessImage(ImageProcessingOptions options, Stream toStream);
-
- /// <summary>
- /// Processes the image.
- /// </summary>
- /// <param name="options">The options.</param>
/// <returns>Task.</returns>
Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options);
@@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing
/// <param name="options">The options.</param>
/// <param name="libraryName">The library name to draw onto the collage.</param>
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
-
- bool SupportsTransparency(string path);
}
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
index 7912c5e87..953cfe698 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
@@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing
private bool IsFormatSupported(string originalImagePath)
{
var ext = Path.GetExtension(originalImagePath);
- return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase));
+ ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase);
+ return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase));
}
}
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
index 62b70ce53..10326363a 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
@@ -9,12 +7,12 @@ namespace MediaBrowser.Controller.Drawing
{
public static class ImageProcessorExtensions
{
- public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
+ public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
{
return processor.GetImageCacheTag(item, imageType, 0);
}
- public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
+ public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
{
var imageInfo = item.GetImageInfo(imageType, imageIndex);
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 2dbd513a1..237345206 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -183,6 +183,9 @@ namespace MediaBrowser.Controller.Entities.Audio
progress.Report(percent * 95);
}
+ // get album LUFS
+ LUFS = items.OfType<Audio>().Max(item => item.LUFS);
+
var parentRefreshOptions = refreshOptions;
if (childUpdateType > ItemUpdateType.None)
{
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 501811003..9f3e8eec9 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1864,7 +1864,7 @@ namespace MediaBrowser.Controller.Entities
/// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
public bool HasImage(ImageType type, int imageIndex)
{
- return GetImageInfo(type, imageIndex) != null;
+ return GetImageInfo(type, imageIndex) is not null;
}
public void SetImage(ItemImageInfo image, int index)
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index 615d236c7..dcd22a3b4 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -95,10 +95,7 @@ namespace MediaBrowser.Controller.Entities
}
var p = destProps.Find(x => x.Name == sourceProp.Name);
- if (p is not null)
- {
- p.SetValue(dest, v);
- }
+ p?.SetValue(dest, v);
}
}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 095b261c0..f51162f9d 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities
public class CollectionFolder : Folder, ICollectionFolder
{
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
+ private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>();
private bool _requiresRefresh;
/// <summary>
@@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities
}
public static LibraryOptions GetLibraryOptions(string path)
- {
- lock (_libraryOptions)
- {
- if (!_libraryOptions.TryGetValue(path, out var options))
- {
- options = LoadLibraryOptions(path);
- _libraryOptions[path] = options;
- }
-
- return options;
- }
- }
+ => _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
public static void SaveLibraryOptions(string path, LibraryOptions options)
{
- lock (_libraryOptions)
- {
- _libraryOptions[path] = options;
+ _libraryOptions[path] = options;
- var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
- foreach (var mediaPath in clone.PathInfos)
+ var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
+ foreach (var mediaPath in clone.PathInfos)
+ {
+ if (!string.IsNullOrEmpty(mediaPath.Path))
{
- if (!string.IsNullOrEmpty(mediaPath.Path))
- {
- mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
- }
+ mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
}
-
- XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
+
+ XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
public static void OnCollectionFolderChange()
- {
- lock (_libraryOptions)
- {
- _libraryOptions.Clear();
- }
- }
+ => _libraryOptions.Clear();
public override bool IsSaveLocalMetadataEnabled()
{
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 44fe65103..e707eedbf 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -598,7 +598,7 @@ namespace MediaBrowser.Controller.Entities
for (var i = 0; i < childrenCount; i++)
{
- await actionBlock.SendAsync(i).ConfigureAwait(false);
+ await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false);
}
actionBlock.Complete();
diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
index 0171af27c..1d45d4da0 100644
--- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs
+++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -14,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
- public string Path { get; set; }
+ public required string Path { get; set; }
/// <summary>
/// Gets or sets the type.
@@ -36,9 +34,9 @@ namespace MediaBrowser.Controller.Entities
/// Gets or sets the blurhash.
/// </summary>
/// <value>The blurhash.</value>
- public string BlurHash { get; set; }
+ public string? BlurHash { get; set; }
[JsonIgnore]
- public bool IsLocalFile => Path is null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
+ public bool IsLocalFile => !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 597b4cecb..bf31508c1 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.Entities.TV
}
[JsonIgnore]
- public bool IsInSeasonFolder => FindParent<Season>() != null;
+ public bool IsInSeasonFolder => FindParent<Season>() is not null;
[JsonIgnore]
public string SeriesPresentationUniqueKey { get; set; }
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 5b7abea10..9f685b7e2 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities
protected override bool IsActiveRecording()
{
- return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+ return LiveTvManager.GetActiveRecordingInfo(Path) is not null;
}
public override bool CanDelete()
diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
new file mode 100644
index 000000000..2742f21e3
--- /dev/null
+++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Xml;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Extensions;
+
+/// <summary>
+/// Provides extension methods for <see cref="XmlReader"/> to parse <see cref="BaseItem"/>'s.
+/// </summary>
+public static class XmlReaderExtensions
+{
+ /// <summary>
+ /// Reads a trimmed string from the current node.
+ /// </summary>
+ /// <param name="reader">The <see cref="XmlReader"/>.</param>
+ /// <returns>The trimmed content.</returns>
+ public static string ReadNormalizedString(this XmlReader reader)
+ {
+ ArgumentNullException.ThrowIfNull(reader);
+
+ return reader.ReadElementContentAsString().Trim();
+ }
+
+ /// <summary>
+ /// Reads an int from the current node.
+ /// </summary>
+ /// <param name="reader">The <see cref="XmlReader"/>.</param>
+ /// <param name="value">The parsed <c>int</c>.</param>
+ /// <returns>A value indicating whether the parsing succeeded.</returns>
+ public static bool TryReadInt(this XmlReader reader, out int value)
+ {
+ ArgumentNullException.ThrowIfNull(reader);
+
+ return int.TryParse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture, out value);
+ }
+
+ /// <summary>
+ /// Parses a <see cref="DateTime"/> from the current node.
+ /// </summary>
+ /// <param name="reader">The <see cref="XmlReader"/>.</param>
+ /// <param name="value">The parsed <see cref="DateTime"/>.</param>
+ /// <returns>A value indicating whether the parsing succeeded.</returns>
+ public static bool TryReadDateTime(this XmlReader reader, out DateTime value)
+ {
+ ArgumentNullException.ThrowIfNull(reader);
+
+ return DateTime.TryParse(
+ reader.ReadElementContentAsString(),
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
+ out value);
+ }
+
+ /// <summary>
+ /// Parses a <see cref="DateTime"/> from the current node.
+ /// </summary>
+ /// <param name="reader">The <see cref="XmlReader"/>.</param>
+ /// <param name="formatString">The date format string.</param>
+ /// <param name="value">The parsed <see cref="DateTime"/>.</param>
+ /// <returns>A value indicating whether the parsing succeeded.</returns>
+ public static bool TryReadDateTimeExact(this XmlReader reader, string formatString, out DateTime value)
+ {
+ ArgumentNullException.ThrowIfNull(reader);
+ ArgumentNullException.ThrowIfNull(formatString);
+
+ return DateTime.TryParseExact(
+ reader.ReadElementContentAsString(),
+ formatString,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
+ out value);
+ }
+
+ /// <summary>
+ /// Parses a <see cref="PersonInfo"/> from the xml node.
+ /// </summary>
+ /// <param name="reader">The <see cref="XmlReader"/>.</param>
+ /// <returns>A <see cref="PersonInfo"/>, or <c>null</c> if none is found.</returns>
+ public static PersonInfo? GetPersonFromXmlNode(this XmlReader reader)
+ {
+ ArgumentNullException.ThrowIfNull(reader);
+
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ return null;
+ }
+
+ var name = string.Empty;
+ var type = PersonKind.Actor; // If type is not specified assume actor
+ var role = string.Empty;
+ int? sortOrder = null;
+ string? imageUrl = null;
+
+ using var subtree = reader.ReadSubtree();
+ subtree.MoveToContent();
+ subtree.Read();
+
+ while (subtree is { EOF: false, ReadState: ReadState.Interactive })
+ {
+ if (subtree.NodeType != XmlNodeType.Element)
+ {
+ subtree.Read();
+ continue;
+ }
+
+ switch (subtree.Name)
+ {
+ case "name":
+ case "Name":
+ name = subtree.ReadNormalizedString();
+ break;
+ case "role":
+ case "Role":
+ role = subtree.ReadNormalizedString();
+ break;
+ case "type":
+ case "Type":
+ Enum.TryParse(subtree.ReadElementContentAsString(), true, out type);
+ break;
+ case "order":
+ case "sortorder":
+ case "SortOrder":
+ if (subtree.TryReadInt(out var sortOrderVal))
+ {
+ sortOrder = sortOrderVal;
+ }
+
+ break;
+ case "thumb":
+ imageUrl = subtree.ReadNormalizedString();
+ break;
+ default:
+ subtree.Skip();
+ break;
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return null;
+ }
+
+ return new PersonInfo
+ {
+ Name = name,
+ Role = role,
+ Type = type,
+ SortOrder = sortOrder,
+ ImageUrl = imageUrl
+ };
+ }
+
+ /// <summary>
+ /// Used to split names of comma or pipe delimited genres and people.
+ /// </summary>
+ /// <param name="reader">The <see cref="XmlReader"/>.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ public static IEnumerable<string> GetStringArray(this XmlReader reader)
+ {
+ ArgumentNullException.ThrowIfNull(reader);
+ var value = reader.ReadElementContentAsString();
+
+ // Only split by comma if there is no pipe in the string
+ // We have to be careful to not split names like Matthew, Jr.
+ var separator = !value.Contains('|', StringComparison.Ordinal)
+ && !value.Contains(';', StringComparison.Ordinal)
+ ? new[] { ',' }
+ : new[] { '|', ';' };
+
+ foreach (var part in value.Trim().Trim(separator).Split(separator))
+ {
+ if (!string.IsNullOrWhiteSpace(part))
+ {
+ yield return part.Trim();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Parses a <see cref="PersonInfo"/> array from the xml node.
+ /// </summary>
+ /// <param name="reader">The <see cref="XmlReader"/>.</param>
+ /// <param name="personKind">The <see cref="PersonKind"/>.</param>
+ /// <returns>The <see cref="IEnumerable{PersonInfo}"/>.</returns>
+ public static IEnumerable<PersonInfo> GetPersonArray(this XmlReader reader, PersonKind personKind)
+ => reader.GetStringArray()
+ .Select(part => new PersonInfo { Name = part, Type = personKind });
+}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 45ac5c3a8..e9c4d9e19 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -4,7 +4,6 @@
using System.Net;
using MediaBrowser.Common;
-using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller
@@ -16,8 +15,6 @@ namespace MediaBrowser.Controller
{
bool CoreStartupHasCompleted { get; }
- bool CanLaunchWebBrowser { get; }
-
/// <summary>
/// Gets the HTTP server port.
/// </summary>
@@ -42,15 +39,6 @@ namespace MediaBrowser.Controller
string FriendlyName { get; }
/// <summary>
- /// Gets the system info.
- /// </summary>
- /// <param name="request">The HTTP request.</param>
- /// <returns>SystemInfo.</returns>
- SystemInfo GetSystemInfo(HttpRequest request);
-
- PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
-
- /// <summary>
/// Gets a URL specific for the request.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/> instance.</param>
diff --git a/MediaBrowser.Controller/ISystemManager.cs b/MediaBrowser.Controller/ISystemManager.cs
new file mode 100644
index 000000000..ef3034d2f
--- /dev/null
+++ b/MediaBrowser.Controller/ISystemManager.cs
@@ -0,0 +1,34 @@
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
+
+namespace MediaBrowser.Controller;
+
+/// <summary>
+/// A service for managing the application instance.
+/// </summary>
+public interface ISystemManager
+{
+ /// <summary>
+ /// Gets the system info.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <returns>The <see cref="SystemInfo"/>.</returns>
+ SystemInfo GetSystemInfo(HttpRequest request);
+
+ /// <summary>
+ /// Gets the public system info.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <returns>The <see cref="PublicSystemInfo"/>.</returns>
+ PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
+
+ /// <summary>
+ /// Starts the application restart process.
+ /// </summary>
+ void Restart();
+
+ /// <summary>
+ /// Starts the application shutdown process.
+ /// </summary>
+ void Shutdown();
+}
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index c70102167..dcd0110fb 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns>
public bool ContainsFileSystemEntryByName(string name)
{
- return GetFileSystemEntryByName(name) != null;
+ return GetFileSystemEntryByName(name) is not null;
}
public string GetCollectionType()
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index c721fb778..05540d490 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.LiveTv
{
public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes
{
- private static string EmbyServiceName = "Emby";
+ private const string EmbyServiceName = "Emby";
public LiveTvProgram()
{
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 69c0d26b6..f9468f6cd 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -49,8 +49,12 @@
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index d61430b0b..6621ae284 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -37,6 +37,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config;
+ private readonly IConfigurationManager _configurationManager;
// i915 hang was fixed by linux 6.2 (3f882f2)
private readonly Version _minKerneli915Hang = new Version(5, 18);
@@ -47,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
+ private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
private static readonly string[] _videoProfilesH264 = new[]
{
@@ -98,6 +100,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "truehd", 6 },
};
+ private static readonly string _defaultMjpegEncoder = "mjpeg";
+ private static readonly Dictionary<string, string> _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "vaapi", _defaultMjpegEncoder + "_vaapi" },
+ { "qsv", _defaultMjpegEncoder + "_qsv" }
+ };
+
public static readonly string[] LosslessAudioCodecs = new string[]
{
"alac",
@@ -112,12 +121,14 @@ namespace MediaBrowser.Controller.MediaEncoding
IApplicationPaths appPaths,
IMediaEncoder mediaEncoder,
ISubtitleEncoder subtitleEncoder,
- IConfiguration config)
+ IConfiguration config,
+ IConfigurationManager configurationManager)
{
_appPaths = appPaths;
_mediaEncoder = mediaEncoder;
_subtitleEncoder = subtitleEncoder;
_config = config;
+ _configurationManager = configurationManager;
}
[GeneratedRegex(@"\s+")]
@@ -163,6 +174,24 @@ namespace MediaBrowser.Controller.MediaEncoding
return defaultEncoder;
}
+ private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ if (state.VideoType == VideoType.VideoFile)
+ {
+ var hwType = encodingOptions.HardwareAccelerationType;
+
+ if (!string.IsNullOrEmpty(hwType)
+ && encodingOptions.EnableHardwareEncoding
+ && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder)
+ && _mediaEncoder.SupportsEncoder(preferredEncoder))
+ {
+ return preferredEncoder;
+ }
+ }
+
+ return _defaultMjpegEncoder;
+ }
+
private bool IsVaapiSupported(EncodingJobInfo state)
{
// vaapi will throw an error with this input
@@ -296,6 +325,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetH264Encoder(state, encodingOptions);
}
+ if (string.Equals(codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetMjpegEncoder(state, encodingOptions);
+ }
+
if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
{
@@ -544,25 +578,25 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.Nullable{VideoCodecs}.</returns>
public string InferVideoCodec(string url)
{
- var ext = Path.GetExtension(url);
+ var ext = Path.GetExtension(url.AsSpan());
- if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase))
{
return "wmv";
}
- if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase))
{
// TODO: this may not always mean VP8, as the codec ages
return "vp8";
}
- if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase))
{
return "theora";
}
- if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase))
{
return "h264";
}
@@ -741,12 +775,17 @@ namespace MediaBrowser.Controller.MediaEncoding
private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias)
{
alias ??= VaapiAlias;
- renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
- var driverOpts = string.IsNullOrEmpty(driver)
- ? ":" + renderNodePath
- : ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
+
+ // 'renderNodePath' has higher priority than 'kernelDriver'
+ var driverOpts = string.IsNullOrEmpty(renderNodePath)
+ ? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver)
+ : renderNodePath;
+
+ // 'driver' behaves similarly to env LIBVA_DRIVER_NAME
+ driverOpts += string.IsNullOrEmpty(driver) ? string.Empty : ",driver=" + driver;
+
var options = string.IsNullOrEmpty(srcDeviceAlias)
- ? driverOpts
+ ? (string.IsNullOrEmpty(driverOpts) ? string.Empty : ":" + driverOpts)
: "@" + srcDeviceAlias;
return string.Format(
@@ -868,14 +907,14 @@ namespace MediaBrowser.Controller.MediaEncoding
if (_mediaEncoder.IsVaapiDeviceInteliHD)
{
- args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, VaapiAlias));
}
else if (_mediaEncoder.IsVaapiDeviceInteli965)
{
// Only override i965 since it has lower priority than iHD in libva lookup.
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
- args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, VaapiAlias));
}
var filterDevArgs = string.Empty;
@@ -1058,7 +1097,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
{
- var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat");
+ var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
arg.Append(" -f concat -safe 0 -i ")
.Append(tmpConcatPath);
@@ -1076,10 +1115,10 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.SubtitleStream.IsExternal)
{
var subtitlePath = state.SubtitleStream.Path;
- var subtitleExtension = Path.GetExtension(subtitlePath);
+ var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
- if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase)
- || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase))
+ if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
+ || subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
{
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
if (File.Exists(idxFile))
@@ -1213,6 +1252,12 @@ namespace MediaBrowser.Controller.MediaEncoding
int bitrate = state.OutputVideoBitrate.Value;
+ // Bit rate under 1000k is not allowed in h264_qsv
+ if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ bitrate = Math.Max(bitrate, 1000);
+ }
+
// Currently use the same buffer size for all encoders
int bufsize = bitrate * 2;
@@ -1735,11 +1780,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Values 0-3, 0 being highest quality but slower
var profileScore = 0;
- string crf;
var qmin = "0";
var qmax = "50";
-
- crf = "10";
+ var crf = "10";
if (isVc1)
{
@@ -1907,7 +1950,9 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(profile))
{
- if (!string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ // Currently there's no profile option in av1_nvenc encoder
+ if (!(string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)))
{
param += " -profile:v:0 " + profile;
}
@@ -1995,6 +2040,14 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0";
}
+ /* Access unit too large: 8192 < 20880 error */
+ if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) &&
+ _mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei)
+ {
+ param += " -sei -a53_cc";
+ }
+
return param;
}
@@ -2692,7 +2745,7 @@ namespace MediaBrowser.Controller.MediaEncoding
string args = string.Empty;
// http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
- if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
+ if (state.VideoStream is not null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
{
int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
@@ -2927,7 +2980,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
+ @"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2",
maxWidthParam,
maxHeightParam,
scaleVal);
@@ -2969,7 +3022,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
+ @"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2",
maxWidthParam,
scaleVal);
}
@@ -2981,7 +3034,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
- "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
+ @"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})",
maxHeightParam,
scaleVal);
}
@@ -3001,19 +3054,19 @@ namespace MediaBrowser.Controller.MediaEncoding
switch (threedFormat.Value)
{
case Video3DFormat.HalfSideBySide:
- filter = "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
+ filter = @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
// hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
break;
case Video3DFormat.FullSideBySide:
- filter = "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
+ filter = @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
// fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to requestedWidth.
break;
case Video3DFormat.HalfTopAndBottom:
- filter = "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
+ filter = @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
// htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth
break;
case Video3DFormat.FullTopAndBottom:
- filter = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
+ filter = @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
// ftab crop height in half, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth
break;
default:
@@ -3811,12 +3864,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// map from d3d11va to qsv.
mainFilters.Add("hwmap=derive_device=qsv");
}
- else
- {
- // Insert a qsv scaler to sync the decoder surface,
- // msdk will passthrough this internally.
- mainFilters.Add("hwmap=derive_device=qsv,scale_qsv");
- }
}
// hw deint
@@ -4905,6 +4952,15 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
overlayFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
+ var framerate = GetFramerateParam(state);
+ if (framerate.HasValue)
+ {
+ mainFilters.Insert(0, string.Format(
+ CultureInfo.InvariantCulture,
+ "fps={0}",
+ framerate.Value));
+ }
+
var mainStr = string.Empty;
if (mainFilters?.Count > 0)
{
@@ -5241,10 +5297,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (isD3d11Supported && isCodecAvailable)
{
- // set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock.
- // on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11.
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
- + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty);
}
}
else
@@ -5678,7 +5732,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// Apply -analyzeduration as per the environment variable,
// otherwise ffmpeg will break on certain files due to default value is 0.
- // The default value of -probesize is more than enough, so leave it as is.
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
if (state.MediaSource.AnalyzeDurationMs > 0)
@@ -5697,6 +5750,14 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier = inputModifier.Trim();
+ // Apply -probesize if configured
+ var ffmpegProbeSize = _config.GetFFmpegProbeSize();
+
+ if (!string.IsNullOrEmpty(ffmpegProbeSize))
+ {
+ inputModifier += $" -probesize {ffmpegProbeSize}";
+ }
+
var userAgentParam = GetUserAgentParam(state);
if (!string.IsNullOrEmpty(userAgentParam))
@@ -6021,7 +6082,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var format = string.Empty;
var keyFrame = string.Empty;
- if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase)
+ if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase)
&& state.BaseRequest.Context == EncodingContext.Streaming)
{
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
@@ -6230,6 +6291,12 @@ namespace MediaBrowser.Controller.MediaEncoding
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
+ if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
+ {
+ audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4)));
+ audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
+ }
+
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
// opus only supports specific sampling rates
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 4114dea4f..c2cef4978 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -138,6 +140,36 @@ namespace MediaBrowser.Controller.MediaEncoding
Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken);
/// <summary>
+ /// Extracts the video images on interval.
+ /// </summary>
+ /// <param name="inputFile">Input file.</param>
+ /// <param name="container">Video container type.</param>
+ /// <param name="mediaSource">Media source information.</param>
+ /// <param name="imageStream">Media stream information.</param>
+ /// <param name="maxWidth">The maximum width.</param>
+ /// <param name="interval">The interval.</param>
+ /// <param name="allowHwAccel">Allow for hardware acceleration.</param>
+ /// <param name="threads">The input/output thread count for ffmpeg.</param>
+ /// <param name="qualityScale">The qscale value for ffmpeg.</param>
+ /// <param name="priority">The process priority for the ffmpeg process.</param>
+ /// <param name="encodingHelper">EncodingHelper instance.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Directory where images where extracted. A given image made before another will always be named with a lower number.</returns>
+ Task<string> ExtractVideoImagesOnIntervalAccelerated(
+ string inputFile,
+ string container,
+ MediaSourceInfo mediaSource,
+ MediaStream imageStream,
+ int maxWidth,
+ TimeSpan interval,
+ bool allowHwAccel,
+ int? threads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
+ EncodingHelper encodingHelper,
+ CancellationToken cancellationToken);
+
+ /// <summary>
/// Gets the media info.
/// </summary>
/// <param name="request">The request.</param>
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index 3b34af4e9..3d288b9f8 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -20,12 +20,12 @@ namespace MediaBrowser.Controller.MediaEncoding
_logger = logger;
}
- public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target)
+ public async Task StartStreamingLog(EncodingJobInfo state, StreamReader reader, Stream target)
{
try
{
using (target)
- using (var reader = new StreamReader(source))
+ using (reader)
{
while (!reader.EndOfStream && reader.BaseStream.CanRead)
{
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index e0942e490..0a706c307 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// The logger.
/// </summary>
- protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
+ protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
{
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index d1a51c2cf..bb68a3b6d 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -44,6 +44,12 @@ namespace MediaBrowser.Controller.Playlists
/// <summary>
/// Gets the playlists folder.
/// </summary>
+ /// <returns>Folder.</returns>
+ Folder GetPlaylistsFolder();
+
+ /// <summary>
+ /// Gets the playlists folder for a user.
+ /// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>Folder.</returns>
Folder GetPlaylistsFolder(Guid userId);
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 16943f6aa..eb5069b06 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index b95d00aa3..282aa721e 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- BaseItem ResolvePath(ItemResolveArgs args);
+ BaseItem? ResolvePath(ItemResolveArgs args);
}
public interface IMultiItemResolver
diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index a6da8384e..5c9dd6f07 100644
--- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- protected internal virtual T Resolve(ItemResolveArgs args)
+ protected internal virtual T? Resolve(ItemResolveArgs args)
{
return null;
}
@@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- public BaseItem ResolvePath(ItemResolveArgs args)
+ public BaseItem? ResolvePath(ItemResolveArgs args)
{
var item = Resolve(args);
diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs
index e3d18c8c0..070ab7a85 100644
--- a/MediaBrowser.Controller/Security/IAuthenticationManager.cs
+++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Security
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 0c4719a0e..53df7133b 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -233,20 +233,6 @@ namespace MediaBrowser.Controller.Session
Task SendRestartRequiredNotification(CancellationToken cancellationToken);
/// <summary>
- /// Sends the server shutdown notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendServerShutdownNotification(CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends the server restart notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendServerRestartNotification(CancellationToken cancellationToken);
-
- /// <summary>
/// Adds the additional user.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 25bf23d61..172d79a59 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -110,6 +110,12 @@ namespace MediaBrowser.Controller.Session
public DateTime LastPlaybackCheckIn { get; set; }
/// <summary>
+ /// Gets or sets the last paused date.
+ /// </summary>
+ /// <value>The last paused date.</value>
+ public DateTime? LastPausedDate { get; set; }
+
+ /// <summary>
/// Gets or sets the name of the device.
/// </summary>
/// <value>The name of the device.</value>
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
new file mode 100644
index 000000000..0c41f3023
--- /dev/null
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Trickplay;
+
+/// <summary>
+/// Interface ITrickplayManager.
+/// </summary>
+public interface ITrickplayManager
+{
+ /// <summary>
+ /// Generates new trickplay images and metadata.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="replace">Whether or not existing data should be replaced.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Task.</returns>
+ Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Creates trickplay tiles out of individual thumbnails.
+ /// </summary>
+ /// <param name="images">Ordered file paths of the thumbnails to be used.</param>
+ /// <param name="width">The width of a single thumbnail.</param>
+ /// <param name="options">The trickplay options.</param>
+ /// <param name="outputDir">The output directory.</param>
+ /// <returns>The associated trickplay information.</returns>
+ /// <remarks>
+ /// The output directory will be DELETED and replaced if it already exists.
+ /// </remarks>
+ TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir);
+
+ /// <summary>
+ /// Get available trickplay resolutions and corresponding info.
+ /// </summary>
+ /// <param name="itemId">The item.</param>
+ /// <returns>Map of width resolutions to trickplay tiles info.</returns>
+ Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId);
+
+ /// <summary>
+ /// Saves trickplay info.
+ /// </summary>
+ /// <param name="info">The trickplay info.</param>
+ /// <returns>Task.</returns>
+ Task SaveTrickplayInfo(TrickplayInfo info);
+
+ /// <summary>
+ /// Gets all trickplay infos for all media streams of an item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>A map of media source id to a map of tile width to trickplay info.</returns>
+ Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item);
+
+ /// <summary>
+ /// Gets the path to a trickplay tile image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="width">The width of a single thumbnail.</param>
+ /// <param name="index">The tile's index.</param>
+ /// <returns>The absolute path.</returns>
+ string GetTrickplayTilePath(BaseItem item, int width, int index);
+
+ /// <summary>
+ /// Gets the trickplay HLS playlist.
+ /// </summary>
+ /// <param name="itemId">The item.</param>
+ /// <param name="width">The width of a single thumbnail.</param>
+ /// <param name="apiKey">Optional api key of the requesting user.</param>
+ /// <returns>The text content of the .m3u8 playlist.</returns>
+ Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey);
+}
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 71cdea529..a39bc238a 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -20,8 +20,12 @@
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index cb369d837..8a870e0d9 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -9,6 +9,7 @@ using System.Xml;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -128,42 +129,19 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
- // DateCreated
case "Added":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
+ if (reader.TryReadDateTime(out var dateCreated))
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added))
- {
- item.DateCreated = added;
- }
- else
- {
- Logger.LogWarning("Invalid Added value found: {Value}", val);
- }
+ item.DateCreated = dateCreated;
}
break;
- }
-
case "OriginalTitle":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrEmpty(val))
- {
- item.OriginalTitle = val;
- }
-
+ item.OriginalTitle = reader.ReadNormalizedString();
break;
- }
-
case "LocalTitle":
- item.Name = reader.ReadElementContentAsString();
+ item.Name = reader.ReadNormalizedString();
break;
-
case "CriticRating":
{
var text = reader.ReadElementContentAsString();
@@ -177,63 +155,26 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
case "SortTitle":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.ForcedSortName = val;
- }
-
+ item.ForcedSortName = reader.ReadNormalizedString();
break;
- }
-
case "Overview":
case "Description":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.Overview = val;
- }
-
+ item.Overview = reader.ReadNormalizedString();
break;
- }
-
case "Language":
- {
- var val = reader.ReadElementContentAsString();
-
- item.PreferredMetadataLanguage = val;
-
+ item.PreferredMetadataLanguage = reader.ReadNormalizedString();
break;
- }
-
case "CountryCode":
- {
- var val = reader.ReadElementContentAsString();
-
- item.PreferredMetadataCountryCode = val;
-
+ item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
break;
- }
-
case "PlaceOfBirth":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
+ var placeOfBirth = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person)
{
- if (item is Person person)
- {
- person.ProductionLocations = new[] { val };
- }
+ person.ProductionLocations = new[] { placeOfBirth };
}
break;
- }
-
case "LockedFields":
{
var val = reader.ReadElementContentAsString();
@@ -275,10 +216,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
if (!reader.IsEmptyElement)
{
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromCountriesNode(subtree);
- }
+ reader.Skip();
}
else
{
@@ -290,183 +228,84 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "ContentRating":
case "MPAARating":
- {
- var rating = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(rating))
- {
- item.OfficialRating = rating;
- }
-
+ item.OfficialRating = reader.ReadNormalizedString();
break;
- }
-
case "CustomRating":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.CustomRating = val;
- }
-
+ item.CustomRating = reader.ReadNormalizedString();
break;
- }
-
case "RunningTime":
- {
- var text = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(text))
+ var runtimeText = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(runtimeText))
{
- if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
+ if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
}
break;
- }
-
case "AspectRatio":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio)
+ var aspectRatio = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
{
- hasAspectRatio.AspectRatio = val;
+ hasAspectRatio.AspectRatio = aspectRatio;
}
break;
- }
-
case "LockData":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
+ item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
break;
- }
-
case "Network":
- {
- foreach (var name in SplitNames(reader.ReadElementContentAsString()))
+ foreach (var name in reader.GetStringArray())
{
- if (string.IsNullOrWhiteSpace(name))
- {
- continue;
- }
-
item.AddStudio(name);
}
break;
- }
-
case "Director":
- {
- foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
+ foreach (var director in reader.GetPersonArray(PersonKind.Director))
{
- if (string.IsNullOrWhiteSpace(p.Name))
- {
- continue;
- }
-
- itemResult.AddPerson(p);
+ itemResult.AddPerson(director);
}
break;
- }
-
case "Writer":
- {
- foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
+ foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
{
- if (string.IsNullOrWhiteSpace(p.Name))
- {
- continue;
- }
-
- itemResult.AddPerson(p);
+ itemResult.AddPerson(writer);
}
break;
- }
-
case "Actors":
- {
- var actors = reader.ReadInnerXml();
-
- if (actors.Contains('<', StringComparison.Ordinal))
+ foreach (var actor in reader.GetPersonArray(PersonKind.Actor))
{
- // This is one of the mis-named "Actors" full nodes created by MB2
- // Create a reader and pass it to the persons node processor
- using var xmlReader = XmlReader.Create(new StringReader($"<Persons>{actors}</Persons>"));
- FetchDataFromPersonsNode(xmlReader, itemResult);
- }
- else
- {
- // Old-style piped string
- foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor }))
- {
- if (string.IsNullOrWhiteSpace(p.Name))
- {
- continue;
- }
-
- itemResult.AddPerson(p);
- }
+ itemResult.AddPerson(actor);
}
break;
- }
-
case "GuestStars":
- {
- foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar }))
+ foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar))
{
- if (string.IsNullOrWhiteSpace(p.Name))
- {
- continue;
- }
-
- itemResult.AddPerson(p);
+ itemResult.AddPerson(guestStar);
}
break;
- }
-
case "Trailer":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
+ var trailer = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(trailer))
{
- item.AddTrailerUrl(val);
+ item.AddTrailerUrl(trailer);
}
break;
- }
-
case "DisplayOrder":
- {
- var val = reader.ReadElementContentAsString();
-
- if (item is IHasDisplayOrder hasDisplayOrder)
+ var displayOrder = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
{
- if (!string.IsNullOrWhiteSpace(val))
- {
- hasDisplayOrder.DisplayOrder = val;
- }
+ hasDisplayOrder.DisplayOrder = displayOrder;
}
break;
- }
-
case "Trailers":
{
if (!reader.IsEmptyElement)
@@ -483,20 +322,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
case "ProductionYear":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
+ if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
{
- if (int.TryParse(val, out var productionYear) && productionYear > 1850)
- {
- item.ProductionYear = productionYear;
- }
+ item.ProductionYear = productionYear;
}
break;
- }
-
case "Rating":
case "IMDBrating":
{
@@ -517,40 +348,24 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "BirthDate":
case "PremiereDate":
case "FirstAired":
- {
- var firstAired = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(firstAired))
+ if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired))
{
- if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
- {
- item.PremiereDate = airDate;
- item.ProductionYear = airDate.Year;
- }
+ item.PremiereDate = firstAired;
+ item.ProductionYear = firstAired.Year;
}
break;
- }
-
case "DeathDate":
case "EndDate":
- {
- var firstAired = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(firstAired))
+ if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate))
{
- if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
- {
- item.EndDate = airDate;
- }
+ item.EndDate = endDate;
}
break;
- }
-
case "CollectionNumber":
- var tmdbCollection = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(tmdbCollection))
+ var tmdbCollection = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(tmdbCollection))
{
item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
}
@@ -753,41 +568,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
item.Shares = list.ToArray();
}
- private void FetchFromCountriesNode(XmlReader reader)
- {
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Country":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- else
- {
- reader.Read();
- }
- }
- }
-
/// <summary>
/// Fetches from taglines node.
/// </summary>
@@ -806,17 +586,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Tagline":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.Tagline = val;
- }
-
+ item.Tagline = reader.ReadNormalizedString();
break;
- }
-
default:
reader.Skip();
break;
@@ -847,17 +618,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Genre":
- {
- var genre = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(genre))
+ var genre = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(genre))
{
item.AddGenre(genre);
}
break;
- }
-
default:
reader.Skip();
break;
@@ -885,17 +652,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Tag":
- {
- var tag = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(tag))
+ var tag = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(tag))
{
tags.Add(tag);
}
break;
- }
-
default:
reader.Skip();
break;
@@ -929,29 +692,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
case "Person":
case "Actor":
- {
- if (reader.IsEmptyElement)
+ var person = reader.GetPersonFromXmlNode();
+ if (person is not null)
{
- reader.Read();
- continue;
- }
-
- using (var subtree = reader.ReadSubtree())
- {
- foreach (var person in GetPersonsFromXmlNode(subtree))
- {
- if (string.IsNullOrWhiteSpace(person.Name))
- {
- continue;
- }
-
- item.AddPerson(person);
- }
+ item.AddPerson(person);
}
break;
- }
-
default:
reader.Skip();
break;
@@ -977,17 +724,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Trailer":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
+ var trailer = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(trailer))
{
- item.AddTrailerUrl(val);
+ item.AddTrailerUrl(trailer);
}
break;
- }
-
default:
reader.Skip();
break;
@@ -1018,17 +761,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Studio":
- {
- var studio = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(studio))
+ var studio = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(studio))
{
item.AddStudio(studio);
}
break;
- }
-
default:
reader.Skip();
break;
@@ -1042,83 +781,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
/// <summary>
- /// Gets the persons from XML node.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <returns>IEnumerable{PersonInfo}.</returns>
- private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
- {
- var name = string.Empty;
- var type = PersonKind.Actor; // If type is not specified assume actor
- var role = string.Empty;
- int? sortOrder = null;
-
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Name":
- name = reader.ReadElementContentAsString();
- break;
-
- case "Type":
- {
- var val = reader.ReadElementContentAsString();
- _ = Enum.TryParse(val, true, out type);
-
- break;
- }
-
- case "Role":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- role = val;
- }
-
- break;
- }
-
- case "SortOrder":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
- {
- sortOrder = intVal;
- }
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- var personInfo = new PersonInfo { Name = name.Trim(), Role = role, Type = type, SortOrder = sortOrder };
-
- return new[] { personInfo };
- }
-
- /// <summary>
/// Get linked child.
/// </summary>
/// <param name="reader">The xml reader.</param>
@@ -1138,17 +800,11 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Path":
- {
- linkedItem.Path = reader.ReadElementContentAsString();
+ linkedItem.Path = reader.ReadNormalizedString();
break;
- }
-
case "ItemId":
- {
- linkedItem.LibraryItemId = reader.ReadElementContentAsString();
+ linkedItem.LibraryItemId = reader.ReadNormalizedString();
break;
- }
-
default:
reader.Skip();
break;
@@ -1189,22 +845,14 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "UserId":
- {
- item.UserId = reader.ReadElementContentAsString();
+ item.UserId = reader.ReadNormalizedString();
break;
- }
-
case "CanEdit":
- {
item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
break;
- }
-
default:
- {
reader.Skip();
break;
- }
}
}
else
@@ -1221,34 +869,5 @@ namespace MediaBrowser.LocalMetadata.Parsers
return null;
}
-
- /// <summary>
- /// Used to split names of comma or pipe delimited genres and people.
- /// </summary>
- /// <param name="value">The value.</param>
- /// <returns>IEnumerable{System.String}.</returns>
- private IEnumerable<string> SplitNames(string value)
- {
- // Only split by comma if there is no pipe in the string
- // We have to be careful to not split names like Matthew, Jr.
- var separator = !value.Contains('|', StringComparison.Ordinal)
- && !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' };
-
- value = value.Trim().Trim(separator);
-
- return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries);
- }
-
- /// <summary>
- /// Provides an additional overload for string.split.
- /// </summary>
- /// <param name="val">The val.</param>
- /// <param name="separators">The separators.</param>
- /// <param name="options">The options.</param>
- /// <returns>System.String[][].</returns>
- private string[] Split(string val, char[] separators, StringSplitOptions options)
- {
- return val.Split(separators, options);
- }
}
}
diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
index 88b190f2b..879a3616b 100644
--- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Xml;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -30,12 +31,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "PlaylistMediaType":
- {
- item.PlaylistMediaType = reader.ReadElementContentAsString();
-
+ item.PlaylistMediaType = reader.ReadNormalizedString();
break;
- }
-
case "PlaylistItems":
if (!reader.IsEmptyElement)
@@ -94,10 +91,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
default:
- {
reader.Skip();
break;
- }
}
}
else
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 989e386a5..299f294b2 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Attachments
{
- public class AttachmentExtractor : IAttachmentExtractor, IDisposable
+ public sealed class AttachmentExtractor : IAttachmentExtractor
{
private readonly ILogger<AttachmentExtractor> _logger;
private readonly IApplicationPaths _appPaths;
@@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
new ConcurrentDictionary<string, SemaphoreSlim>();
- private bool _disposed = false;
-
public AttachmentExtractor(
ILogger<AttachmentExtractor> logger,
IApplicationPaths appPaths,
@@ -177,22 +174,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
process.Start();
- var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
-
- if (!ranToCompletion)
+ try
{
- try
- {
- _logger.LogWarning("Killing ffmpeg attachment extraction process");
- process.Kill();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error killing attachment extraction process");
- }
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ exitCode = process.ExitCode;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ exitCode = -1;
}
-
- exitCode = ranToCompletion ? process.ExitCode : -1;
}
var failed = false;
@@ -296,7 +287,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
ArgumentException.ThrowIfNullOrEmpty(outputPath);
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
var processArgs = string.Format(
CultureInfo.InvariantCulture,
@@ -325,22 +316,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
process.Start();
- var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
-
- if (!ranToCompletion)
+ try
{
- try
- {
- _logger.LogWarning("Killing ffmpeg attachment extraction process");
- process.Kill();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error killing attachment extraction process");
- }
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ exitCode = process.ExitCode;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ exitCode = -1;
}
-
- exitCode = ranToCompletion ? process.ExitCode : -1;
}
var failed = false;
@@ -391,33 +376,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
}
- var prefix = filename.Substring(0, 1);
- return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
- }
-
- /// <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)
- {
- }
-
- _disposed = true;
+ var prefix = filename.AsSpan(0, 1);
+ return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
}
}
}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index 9e7a1d50a..1f94d9b23 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo;
/// </summary>
public class BdInfoFileInfo : BDInfo.IO.IFileInfo
{
- private FileSystemMetadata _impl;
+ private readonly FileSystemMetadata _impl;
/// <summary>
/// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class.
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 38118ed0e..f12ef7e63 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -499,8 +499,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders;
- var found = Regex
- .Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline)
+ var found = CodecRegex()
+ .Matches(output)
.Select(x => x.Groups["codec"].Value)
.Where(x => required.Contains(x));
@@ -527,8 +527,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
return Enumerable.Empty<string>();
}
- var found = Regex
- .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline)
+ var found = FilterRegex()
+ .Matches(output)
.Select(x => x.Groups["filter"].Value)
.Where(x => _requiredFilters.Contains(x));
@@ -553,7 +553,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
{
- using (var process = new Process()
+ var redirectStandardIn = !string.IsNullOrEmpty(testKey);
+ using (var process = new Process
{
StartInfo = new ProcessStartInfo(path, arguments)
{
@@ -561,7 +562,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false,
- RedirectStandardInput = !string.IsNullOrEmpty(testKey),
+ RedirectStandardInput = redirectStandardIn,
RedirectStandardOutput = true,
RedirectStandardError = true
}
@@ -571,13 +572,21 @@ namespace MediaBrowser.MediaEncoding.Encoder
process.Start();
- if (!string.IsNullOrEmpty(testKey))
+ if (redirectStandardIn)
{
- process.StandardInput.Write(testKey);
+ using var writer = process.StandardInput;
+ writer.Write(testKey);
}
- return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd();
+ using var reader = readStdErr ? process.StandardError : process.StandardOutput;
+ return reader.ReadToEnd();
}
}
+
+ [GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
+ private static partial Regex CodecRegex();
+
+ [GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
+ private static partial Regex FilterRegex();
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 9d6cdf728..4dbefca4b 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -21,6 +21,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Probing;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -28,8 +29,10 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
+using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static Nikse.SubtitleEdit.Core.Common.IfoParser;
namespace MediaBrowser.MediaEncoding.Encoder
{
@@ -177,7 +180,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (_ffmpegPath is not null)
{
// Determine a probe path from the mpeg path
- _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, @"ffprobe$1");
+ _ffprobePath = FfprobePathRegex().Replace(_ffmpegPath, "ffprobe$1");
// Interrogate to understand what coders are supported
var validator = new EncoderValidator(_logger, _ffmpegPath);
@@ -316,10 +319,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
var files = _fileSystem.GetFilePaths(path, recursive);
- var excludeExtensions = new[] { ".c" };
-
- return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
- && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
+ return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase)
+ && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase));
}
catch (Exception)
{
@@ -417,27 +418,51 @@ namespace MediaBrowser.MediaEncoding.Encoder
public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
{
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
- var analyzeDuration = string.Empty;
+ var extraArgs = GetExtraArguments(request);
+
+ return GetMediaInfoInternal(
+ GetInputArgument(request.MediaSource.Path, request.MediaSource),
+ request.MediaSource.Path,
+ request.MediaSource.Protocol,
+ extractChapters,
+ extraArgs,
+ request.MediaType == DlnaProfileType.Audio,
+ request.MediaSource.VideoType,
+ cancellationToken);
+ }
+
+ internal string GetExtraArguments(MediaInfoRequest request)
+ {
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
+ var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
+ var analyzeDuration = string.Empty;
+ var extraArgs = string.Empty;
if (request.MediaSource.AnalyzeDurationMs > 0)
{
- analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
+ analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000);
}
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
{
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
}
- return GetMediaInfoInternal(
- GetInputArgument(request.MediaSource.Path, request.MediaSource),
- request.MediaSource.Path,
- request.MediaSource.Protocol,
- extractChapters,
- analyzeDuration,
- request.MediaType == DlnaProfileType.Audio,
- request.MediaSource.VideoType,
- cancellationToken);
+ if (!string.IsNullOrEmpty(analyzeDuration))
+ {
+ extraArgs = analyzeDuration;
+ }
+
+ if (!string.IsNullOrEmpty(ffmpegProbeSize))
+ {
+ extraArgs += " -probesize " + ffmpegProbeSize;
+ }
+
+ if (request.MediaSource.RequiredHttpHeaders.TryGetValue("user_agent", out var userAgent))
+ {
+ extraArgs += " -user_agent " + userAgent;
+ }
+
+ return extraArgs;
}
/// <inheritdoc />
@@ -511,7 +536,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
using (var processWrapper = new ProcessWrapper(process, this))
{
StartProcess(processWrapper);
- await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
+ using var reader = process.StandardOutput;
+ await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
memoryStream.Seek(0, SeekOrigin.Begin);
InternalMediaInfoResult result;
try
@@ -612,9 +638,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private string GetImageResolutionParameter()
{
- string imageResolutionParameter;
-
- imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
+ var imageResolutionParameter = _serverConfig.Configuration.ChapterImageResolution switch
{
ImageResolution.P144 => "256x144",
ImageResolution.P240 => "426x240",
@@ -639,15 +663,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
ArgumentException.ThrowIfNullOrEmpty(inputPath);
- var outputExtension = targetFormat switch
- {
- ImageFormat.Bmp => ".bmp",
- ImageFormat.Gif => ".gif",
- ImageFormat.Jpg => ".jpg",
- ImageFormat.Png => ".png",
- ImageFormat.Webp => ".webp",
- _ => ".jpg"
- };
+ var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
@@ -667,13 +683,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
var scaler = threedFormat switch
{
// hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
- Video3DFormat.HalfSideBySide => "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
+ Video3DFormat.HalfSideBySide => @"crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
// fsbs crop width in half,set the display aspect,crop out any black bars we may have made
- Video3DFormat.FullSideBySide => "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
+ Video3DFormat.FullSideBySide => @"crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
// htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made
- Video3DFormat.HalfTopAndBottom => "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
+ Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
// ftab crop height in half, set the display aspect,crop out any black bars we may have made
- Video3DFormat.FullTopAndBottom => "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
+ Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
_ => "scale=trunc(iw*sar):ih"
};
@@ -749,11 +765,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout;
}
- ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
-
- if (!ranToCompletion)
+ try
{
- StopProcess(processWrapper, 1000);
+ await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
+ ranToCompletion = true;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ ranToCompletion = false;
}
}
finally
@@ -776,6 +796,191 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
/// <inheritdoc />
+ public Task<string> ExtractVideoImagesOnIntervalAccelerated(
+ string inputFile,
+ string container,
+ MediaSourceInfo mediaSource,
+ MediaStream imageStream,
+ int maxWidth,
+ TimeSpan interval,
+ bool allowHwAccel,
+ int? threads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
+ EncodingHelper encodingHelper,
+ CancellationToken cancellationToken)
+ {
+ var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
+ threads ??= _threads;
+
+ // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
+ // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
+ if (!allowHwAccel)
+ {
+ options.EnableHardwareEncoding = false;
+ options.HardwareAccelerationType = string.Empty;
+ options.EnableTonemapping = false;
+ }
+
+ var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
+ var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
+ {
+ IsVideoRequest = true, // must be true for InputVideoHwaccelArgs to return non-empty value
+ MediaSource = mediaSource,
+ VideoStream = imageStream,
+ BaseRequest = baseRequest, // GetVideoProcessingFilterParam errors if null
+ MediaPath = inputFile,
+ OutputVideoCodec = "mjpeg"
+ };
+ var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec;
+
+ // Get input and filter arguments
+ var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
+ if (string.IsNullOrWhiteSpace(inputArg))
+ {
+ throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
+ }
+
+ if (!allowHwAccel)
+ {
+ inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
+ }
+
+ var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim();
+ if (string.IsNullOrWhiteSpace(filterParam))
+ {
+ throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
+ }
+
+ return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken);
+ }
+
+ private async Task<string> ExtractVideoImagesOnIntervalInternal(
+ string inputArg,
+ string filterParam,
+ string vidEncoder,
+ int? outputThreads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(inputArg))
+ {
+ throw new InvalidOperationException("Empty or invalid input argument.");
+ }
+
+ // Output arguments
+ var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(targetDirectory);
+ var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
+
+ // Final command arguments
+ var args = string.Format(
+ CultureInfo.InvariantCulture,
+ "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}-f {5} \"{6}\"",
+ inputArg,
+ filterParam,
+ outputThreads.GetValueOrDefault(_threads),
+ vidEncoder,
+ qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
+ "image2",
+ outputPath);
+
+ // Start ffmpeg process
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = _ffmpegPath,
+ Arguments = args,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false,
+ },
+ EnableRaisingEvents = true
+ };
+
+ var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+ _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
+
+ using (var processWrapper = new ProcessWrapper(process, this))
+ {
+ bool ranToCompletion = false;
+
+ await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ StartProcess(processWrapper);
+
+ // Set process priority
+ if (priority.HasValue)
+ {
+ try
+ {
+ processWrapper.Process.PriorityClass = priority.Value;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", priority.Value, processDescription);
+ }
+ }
+
+ // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
+ // but we still need to detect if the process hangs.
+ // Making the assumption that as long as new jpegs are showing up, everything is good.
+
+ bool isResponsive = true;
+ int lastCount = 0;
+ var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
+ timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
+
+ while (isResponsive)
+ {
+ try
+ {
+ await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
+
+ ranToCompletion = true;
+ break;
+ }
+ catch (OperationCanceledException)
+ {
+ // We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
+
+ isResponsive = jpegCount > lastCount;
+ lastCount = jpegCount;
+ }
+
+ if (!ranToCompletion)
+ {
+ _logger.LogInformation("Stopping trickplay extraction due to process inactivity.");
+ StopProcess(processWrapper, 1000);
+ }
+ }
+ finally
+ {
+ _thumbnailResourcePool.Release();
+ }
+
+ var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
+
+ if (exitCode == -1)
+ {
+ _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
+
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
+ }
+
+ return targetDirectory;
+ }
+ }
+
public string GetTimeParameter(long ticks)
{
var time = TimeSpan.FromTicks(ticks);
@@ -845,7 +1050,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
// We need to double escape
- return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", "'\\\\\\''", StringComparison.Ordinal);
+ return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringComparison.Ordinal);
}
/// <inheritdoc />
@@ -988,7 +1193,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return true;
}
- private class ProcessWrapper : IDisposable
+ private sealed class ProcessWrapper : IDisposable
{
private readonly MediaEncoder _mediaEncoder;
@@ -1031,13 +1236,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_mediaEncoder._runningProcesses.Remove(this);
}
- try
- {
- process.Dispose();
- }
- catch
- {
- }
+ process.Dispose();
}
public void Dispose()
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index a0624fe76..1f39e88cd 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -29,8 +29,12 @@
<PackageReference Include="UTF.Unknown" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index aeb08cea3..be1e8a172 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <summary>
/// Class responsible for normalizing FFprobe output.
/// </summary>
- public class ProbeResultNormalizer
+ public partial class ProbeResultNormalizer
{
// When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
private const int MaxSubtitleDescriptionExtractionLength = 100;
@@ -31,8 +31,6 @@ namespace MediaBrowser.MediaEncoding.Probing
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
- private static readonly Regex _performerPattern = new(@"(?<name>.*) \((?<instrument>.*)\)");
-
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
@@ -78,6 +76,7 @@ namespace MediaBrowser.MediaEncoding.Probing
"She/Her/Hers",
"5/8erl in Ehr'n",
"Smith/Kotzen",
+ "We;Na",
};
/// <summary>
@@ -762,9 +761,11 @@ namespace MediaBrowser.MediaEncoding.Probing
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
if (isAudio
- || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
- || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
- || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
+ && (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
{
stream.Type = MediaStreamType.EmbeddedImage;
}
@@ -1212,7 +1213,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(performer, false))
{
- Match match = _performerPattern.Match(person);
+ Match match = PerformerRegex().Match(person);
// If the performer doesn't have any instrument/role associated, it won't match. In that case, chances are it's simply a band name, so we skip it.
if (match.Success)
@@ -1651,5 +1652,8 @@ namespace MediaBrowser.MediaEncoding.Probing
return TransportStreamTimestamp.Valid;
}
+
+ [GeneratedRegex("(?<name>.*) \\((?<instrument>.*)\\)")]
+ private static partial Regex PerformerRegex();
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index a41e0b7e9..8eea773d8 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -135,19 +135,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
- var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
+ var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
.ConfigureAwait(false);
- var inputFormat = subtitle.Format;
-
// Return the original if the same format is being requested
// Character encoding was already handled in GetSubtitleStream
if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
{
- return subtitle.Stream;
+ return stream;
}
- using (var stream = subtitle.Stream)
+ using (stream)
{
return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
}
@@ -420,23 +418,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw;
}
- var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
-
- if (!ranToCompletion)
+ try
{
- try
- {
- _logger.LogInformation("Killing ffmpeg subtitle conversion process");
-
- process.Kill();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error killing subtitle conversion process");
- }
+ await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
+ exitCode = process.ExitCode;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ exitCode = -1;
}
-
- exitCode = ranToCompletion ? process.ExitCode : -1;
}
var failed = false;
@@ -574,23 +565,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw;
}
- var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
-
- if (!ranToCompletion)
+ try
{
- try
- {
- _logger.LogWarning("Killing ffmpeg subtitle extraction process");
-
- process.Kill();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error killing subtitle extraction process");
- }
+ await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
+ exitCode = process.ExitCode;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ exitCode = -1;
}
-
- exitCode = ranToCompletion ? process.ExitCode : -1;
}
var failed = false;
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 3f0e98ec8..84c735f9c 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -50,6 +50,7 @@ public class EncodingOptions
EnableHardwareEncoding = true;
AllowHevcEncoding = false;
AllowAv1Encoding = false;
+ AllowMjpegEncoding = false;
EnableSubtitleExtraction = true;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
@@ -256,6 +257,11 @@ public class EncodingOptions
public bool AllowAv1Encoding { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether MJPEG encoding is enabled.
+ /// </summary>
+ public bool AllowMjpegEncoding { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether subtitle extraction is enabled.
/// </summary>
public bool EnableSubtitleExtraction { get; set; }
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 9743edb1c..fbad29143 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -35,6 +35,10 @@ namespace MediaBrowser.Model.Configuration
public bool ExtractChapterImagesDuringLibraryScan { get; set; }
+ public bool EnableTrickplayImageExtraction { get; set; }
+
+ public bool ExtractTrickplayImagesDuringLibraryScan { get; set; }
+
public MediaPathInfo[] PathInfos { get; set; }
public bool SaveLocalMetadata { get; set; }
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 78a310f0b..fe92251e9 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -4,265 +4,283 @@
using System;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.System;
using MediaBrowser.Model.Updates;
-namespace MediaBrowser.Model.Configuration
+namespace MediaBrowser.Model.Configuration;
+
+/// <summary>
+/// Represents the server configuration.
+/// </summary>
+public class ServerConfiguration : BaseApplicationConfiguration
{
/// <summary>
- /// Represents the server configuration.
+ /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
/// </summary>
- public class ServerConfiguration : BaseApplicationConfiguration
+ public ServerConfiguration()
{
- /// <summary>
- /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
- /// </summary>
- public ServerConfiguration()
+ MetadataOptions = new[]
{
- MetadataOptions = new[]
+ new MetadataOptions()
+ {
+ ItemType = "Book"
+ },
+ new MetadataOptions()
+ {
+ ItemType = "Movie"
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicVideo",
+ DisabledMetadataFetchers = new[] { "The Open Movie Database" },
+ DisabledImageFetchers = new[] { "The Open Movie Database" }
+ },
+ new MetadataOptions
+ {
+ ItemType = "Series",
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicAlbum",
+ DisabledMetadataFetchers = new[] { "TheAudioDB" }
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicArtist",
+ DisabledMetadataFetchers = new[] { "TheAudioDB" }
+ },
+ new MetadataOptions
+ {
+ ItemType = "BoxSet"
+ },
+ new MetadataOptions
{
- new MetadataOptions()
- {
- ItemType = "Book"
- },
- new MetadataOptions()
- {
- ItemType = "Movie"
- },
- new MetadataOptions
- {
- ItemType = "MusicVideo",
- DisabledMetadataFetchers = new[] { "The Open Movie Database" },
- DisabledImageFetchers = new[] { "The Open Movie Database" }
- },
- new MetadataOptions
- {
- ItemType = "Series",
- },
- new MetadataOptions
- {
- ItemType = "MusicAlbum",
- DisabledMetadataFetchers = new[] { "TheAudioDB" }
- },
- new MetadataOptions
- {
- ItemType = "MusicArtist",
- DisabledMetadataFetchers = new[] { "TheAudioDB" }
- },
- new MetadataOptions
- {
- ItemType = "BoxSet"
- },
- new MetadataOptions
- {
- ItemType = "Season",
- },
- new MetadataOptions
- {
- ItemType = "Episode",
- }
- };
- }
-
- /// <summary>
- /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
- /// </summary>
- public bool EnableMetrics { get; set; } = false;
-
- public bool EnableNormalizedItemByNameIds { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is port authorized.
- /// </summary>
- /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value>
- public bool IsPortAuthorized { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether quick connect is available for use on this server.
- /// </summary>
- public bool QuickConnectAvailable { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating whether [enable case sensitive item ids].
- /// </summary>
- /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value>
- public bool EnableCaseSensitiveItemIds { get; set; } = true;
-
- public bool DisableLiveTvChannelUserDataName { get; set; } = true;
-
- /// <summary>
- /// Gets or sets the metadata path.
- /// </summary>
- /// <value>The metadata path.</value>
- public string MetadataPath { get; set; } = string.Empty;
-
- public string MetadataNetworkPath { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the preferred metadata language.
- /// </summary>
- /// <value>The preferred metadata language.</value>
- public string PreferredMetadataLanguage { get; set; } = "en";
-
- /// <summary>
- /// Gets or sets the metadata country code.
- /// </summary>
- /// <value>The metadata country code.</value>
- public string MetadataCountryCode { get; set; } = "US";
-
- /// <summary>
- /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name.
- /// </summary>
- /// <value>The sort replace characters.</value>
- public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" };
-
- /// <summary>
- /// Gets or sets characters to be removed from strings to create a sort name.
- /// </summary>
- /// <value>The sort remove characters.</value>
- public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" };
-
- /// <summary>
- /// Gets or sets words to be removed from strings to create a sort name.
- /// </summary>
- /// <value>The sort remove words.</value>
- public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" };
-
- /// <summary>
- /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
- /// </summary>
- /// <value>The min resume PCT.</value>
- public int MinResumePct { get; set; } = 5;
-
- /// <summary>
- /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
- /// </summary>
- /// <value>The max resume PCT.</value>
- public int MaxResumePct { get; set; } = 90;
-
- /// <summary>
- /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
- /// </summary>
- /// <value>The min resume duration seconds.</value>
- public int MinResumeDurationSeconds { get; set; } = 300;
-
- /// <summary>
- /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.
- /// </summary>
- /// <value>The min resume in minutes.</value>
- public int MinAudiobookResume { get; set; } = 5;
-
- /// <summary>
- /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
- /// </summary>
- /// <value>The remaining time in minutes.</value>
- public int MaxAudiobookResume { get; set; } = 5;
-
- /// <summary>
- /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
- /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
- /// different directories and files.
- /// </summary>
- /// <value>The file watcher delay.</value>
- public int LibraryMonitorDelay { get; set; } = 60;
-
- /// <summary>
- /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.
- /// </summary>
- /// <value>The library update duration.</value>
- public int LibraryUpdateDuration { get; set; } = 30;
-
- /// <summary>
- /// Gets or sets the image saving convention.
- /// </summary>
- /// <value>The image saving convention.</value>
- public ImageSavingConvention ImageSavingConvention { get; set; }
-
- public MetadataOptions[] MetadataOptions { get; set; }
-
- public bool SkipDeserializationForBasicTypes { get; set; } = true;
-
- public string ServerName { get; set; } = string.Empty;
-
- public string UICulture { get; set; } = "en-US";
-
- public bool SaveMetadataHidden { get; set; } = false;
-
- public NameValuePair[] ContentTypes { get; set; } = Array.Empty<NameValuePair>();
-
- public int RemoteClientBitrateLimit { get; set; }
-
- public bool EnableFolderView { get; set; } = false;
-
- public bool EnableGroupingIntoCollections { get; set; } = false;
-
- public bool DisplaySpecialsWithinSeasons { get; set; } = true;
-
- public string[] CodecsUsed { get; set; } = Array.Empty<string>();
-
- public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty<RepositoryInfo>();
-
- public bool EnableExternalContentInSuggestions { get; set; } = true;
-
- public int ImageExtractionTimeoutMs { get; set; }
-
- public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
-
- /// <summary>
- /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
- /// </summary>
- public bool EnableSlowResponseWarning { get; set; } = true;
-
- /// <summary>
- /// Gets or sets the threshold for the slow response time warning in ms.
- /// </summary>
- public long SlowResponseThresholdMs { get; set; } = 500;
-
- /// <summary>
- /// Gets or sets the cors hosts.
- /// </summary>
- public string[] CorsHosts { get; set; } = new[] { "*" };
-
- /// <summary>
- /// Gets or sets the number of days we should retain activity logs.
- /// </summary>
- public int? ActivityLogRetentionDays { get; set; } = 30;
+ ItemType = "Season",
+ },
+ new MetadataOptions
+ {
+ ItemType = "Episode",
+ }
+ };
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
+ /// </summary>
+ public bool EnableMetrics { get; set; } = false;
+
+ public bool EnableNormalizedItemByNameIds { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is port authorized.
+ /// </summary>
+ /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value>
+ public bool IsPortAuthorized { get; set; }
- /// <summary>
- /// Gets or sets the how the library scan fans out.
- /// </summary>
- public int LibraryScanFanoutConcurrency { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether quick connect is available for use on this server.
+ /// </summary>
+ public bool QuickConnectAvailable { get; set; } = true;
- /// <summary>
- /// Gets or sets the how many metadata refreshes can run concurrently.
- /// </summary>
- public int LibraryMetadataRefreshConcurrency { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable case sensitive item ids].
+ /// </summary>
+ /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value>
+ public bool EnableCaseSensitiveItemIds { get; set; } = true;
- /// <summary>
- /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
- /// </summary>
- public bool RemoveOldPlugins { get; set; }
+ public bool DisableLiveTvChannelUserDataName { get; set; } = true;
- /// <summary>
- /// Gets or sets a value indicating whether clients should be allowed to upload logs.
- /// </summary>
- public bool AllowClientLogUpload { get; set; } = true;
+ /// <summary>
+ /// Gets or sets the metadata path.
+ /// </summary>
+ /// <value>The metadata path.</value>
+ public string MetadataPath { get; set; } = string.Empty;
- /// <summary>
- /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
- /// </summary>
- /// <value>The dummy chapters duration.</value>
- public int DummyChapterDuration { get; set; }
+ public string MetadataNetworkPath { get; set; } = string.Empty;
- /// <summary>
- /// Gets or sets the chapter image resolution.
- /// </summary>
- /// <value>The chapter image resolution.</value>
- public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
+ /// <summary>
+ /// Gets or sets the preferred metadata language.
+ /// </summary>
+ /// <value>The preferred metadata language.</value>
+ public string PreferredMetadataLanguage { get; set; } = "en";
- /// <summary>
- /// Gets or sets the limit for parallel image encoding.
- /// </summary>
- /// <value>The limit for parallel image encoding.</value>
- public int ParallelImageEncodingLimit { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the metadata country code.
+ /// </summary>
+ /// <value>The metadata country code.</value>
+ public string MetadataCountryCode { get; set; } = "US";
+
+ /// <summary>
+ /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name.
+ /// </summary>
+ /// <value>The sort replace characters.</value>
+ public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" };
+
+ /// <summary>
+ /// Gets or sets characters to be removed from strings to create a sort name.
+ /// </summary>
+ /// <value>The sort remove characters.</value>
+ public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" };
+
+ /// <summary>
+ /// Gets or sets words to be removed from strings to create a sort name.
+ /// </summary>
+ /// <value>The sort remove words.</value>
+ public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" };
+
+ /// <summary>
+ /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
+ /// </summary>
+ /// <value>The min resume PCT.</value>
+ public int MinResumePct { get; set; } = 5;
+
+ /// <summary>
+ /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ /// </summary>
+ /// <value>The max resume PCT.</value>
+ public int MaxResumePct { get; set; } = 90;
+
+ /// <summary>
+ /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
+ /// </summary>
+ /// <value>The min resume duration seconds.</value>
+ public int MinResumeDurationSeconds { get; set; } = 300;
+
+ /// <summary>
+ /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.
+ /// </summary>
+ /// <value>The min resume in minutes.</value>
+ public int MinAudiobookResume { get; set; } = 5;
+
+ /// <summary>
+ /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ /// </summary>
+ /// <value>The remaining time in minutes.</value>
+ public int MaxAudiobookResume { get; set; } = 5;
+
+ /// <summary>
+ /// Gets or sets the threshold in minutes after a inactive session gets closed automatically.
+ /// If set to 0 the check for inactive sessions gets disabled.
+ /// </summary>
+ /// <value>The close inactive session threshold in minutes. 0 to disable.</value>
+ public int InactiveSessionThreshold { get; set; } = 10;
+
+ /// <summary>
+ /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
+ /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
+ /// different directories and files.
+ /// </summary>
+ /// <value>The file watcher delay.</value>
+ public int LibraryMonitorDelay { get; set; } = 60;
+
+ /// <summary>
+ /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.
+ /// </summary>
+ /// <value>The library update duration.</value>
+ public int LibraryUpdateDuration { get; set; } = 30;
+
+ /// <summary>
+ /// Gets or sets the image saving convention.
+ /// </summary>
+ /// <value>The image saving convention.</value>
+ public ImageSavingConvention ImageSavingConvention { get; set; }
+
+ public MetadataOptions[] MetadataOptions { get; set; }
+
+ public bool SkipDeserializationForBasicTypes { get; set; } = true;
+
+ public string ServerName { get; set; } = string.Empty;
+
+ public string UICulture { get; set; } = "en-US";
+
+ public bool SaveMetadataHidden { get; set; } = false;
+
+ public NameValuePair[] ContentTypes { get; set; } = Array.Empty<NameValuePair>();
+
+ public int RemoteClientBitrateLimit { get; set; }
+
+ public bool EnableFolderView { get; set; } = false;
+
+ public bool EnableGroupingIntoCollections { get; set; } = false;
+
+ public bool DisplaySpecialsWithinSeasons { get; set; } = true;
+
+ public string[] CodecsUsed { get; set; } = Array.Empty<string>();
+
+ public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty<RepositoryInfo>();
+
+ public bool EnableExternalContentInSuggestions { get; set; } = true;
+
+ public int ImageExtractionTimeoutMs { get; set; }
+
+ public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
+ /// </summary>
+ public bool EnableSlowResponseWarning { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the threshold for the slow response time warning in ms.
+ /// </summary>
+ public long SlowResponseThresholdMs { get; set; } = 500;
+
+ /// <summary>
+ /// Gets or sets the cors hosts.
+ /// </summary>
+ public string[] CorsHosts { get; set; } = new[] { "*" };
+
+ /// <summary>
+ /// Gets or sets the number of days we should retain activity logs.
+ /// </summary>
+ public int? ActivityLogRetentionDays { get; set; } = 30;
+
+ /// <summary>
+ /// Gets or sets the how the library scan fans out.
+ /// </summary>
+ public int LibraryScanFanoutConcurrency { get; set; }
+
+ /// <summary>
+ /// Gets or sets the how many metadata refreshes can run concurrently.
+ /// </summary>
+ public int LibraryMetadataRefreshConcurrency { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
+ /// </summary>
+ public bool RemoveOldPlugins { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether clients should be allowed to upload logs.
+ /// </summary>
+ public bool AllowClientLogUpload { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
+ /// </summary>
+ /// <value>The dummy chapters duration.</value>
+ public int DummyChapterDuration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the chapter image resolution.
+ /// </summary>
+ /// <value>The chapter image resolution.</value>
+ public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
+
+ /// <summary>
+ /// Gets or sets the limit for parallel image encoding.
+ /// </summary>
+ /// <value>The limit for parallel image encoding.</value>
+ public int ParallelImageEncodingLimit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of cast receiver applications.
+ /// </summary>
+ public CastReceiverApplication[] CastReceiverApplications { get; set; } = Array.Empty<CastReceiverApplication>();
+
+ /// <summary>
+ /// Gets or sets the trickplay options.
+ /// </summary>
+ /// <value>The trickplay options.</value>
+ public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions();
}
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
new file mode 100644
index 000000000..92c16ee84
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace MediaBrowser.Model.Configuration;
+
+/// <summary>
+/// Class TrickplayOptions.
+/// </summary>
+public class TrickplayOptions
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether or not to use HW acceleration.
+ /// </summary>
+ public bool EnableHwAcceleration { get; set; } = false;
+
+ /// <summary>
+ /// Gets or sets the behavior used by trickplay provider on library scan/update.
+ /// </summary>
+ public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
+
+ /// <summary>
+ /// Gets or sets the process priority for the ffmpeg process.
+ /// </summary>
+ public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
+
+ /// <summary>
+ /// Gets or sets the interval, in ms, between each new trickplay image.
+ /// </summary>
+ public int Interval { get; set; } = 10000;
+
+ /// <summary>
+ /// Gets or sets the target width resolutions, in px, to generates preview images for.
+ /// </summary>
+ public int[] WidthResolutions { get; set; } = new[] { 320 };
+
+ /// <summary>
+ /// Gets or sets number of tile images to allow in X dimension.
+ /// </summary>
+ public int TileWidth { get; set; } = 10;
+
+ /// <summary>
+ /// Gets or sets number of tile images to allow in Y dimension.
+ /// </summary>
+ public int TileHeight { get; set; } = 10;
+
+ /// <summary>
+ /// Gets or sets the ffmpeg output quality level.
+ /// </summary>
+ public int Qscale { get; set; } = 4;
+
+ /// <summary>
+ /// Gets or sets the jpeg quality to use for image tiles.
+ /// </summary>
+ public int JpegQuality { get; set; } = 90;
+
+ /// <summary>
+ /// Gets or sets the number of threads to be used by ffmpeg.
+ /// </summary>
+ public int ProcessThreads { get; set; } = 1;
+}
diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
new file mode 100644
index 000000000..d0db53218
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Configuration;
+
+/// <summary>
+/// Enum TrickplayScanBehavior.
+/// </summary>
+public enum TrickplayScanBehavior
+{
+ /// <summary>
+ /// Starts generation, only return once complete.
+ /// </summary>
+ Blocking,
+
+ /// <summary>
+ /// Start generation, return immediately.
+ /// </summary>
+ NonBlocking
+}
diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs
index 94f354660..b477f2593 100644
--- a/MediaBrowser.Model/Configuration/UserConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs
@@ -69,5 +69,10 @@ namespace MediaBrowser.Model.Configuration
public bool RememberSubtitleSelections { get; set; }
public bool EnableNextEpisodeAutoPlay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the selected cast receiver.
+ /// </summary>
+ public string? CastReceiverId { get; set; }
}
}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index b7c23669d..07bb002ea 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -314,7 +314,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="audioSampleRate">The audio sample rate.</param>
/// <param name="audioBitDepth">The audio bit depth.</param>
/// <returns>The <see cref="ResponseProfile"/>.</returns>
- public ResponseProfile? GetAudioMediaProfile(string container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
+ public ResponseProfile? GetAudioMediaProfile(string? container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
{
foreach (var i in ResponseProfiles)
{
@@ -438,14 +438,14 @@ namespace MediaBrowser.Model.Dlna
/// <param name="isAvc">True if Avc.</param>
/// <returns>The <see cref="ResponseProfile"/>.</returns>
public ResponseProfile? GetVideoMediaProfile(
- string container,
+ string? container,
string? audioCodec,
string? videoCodec,
int? width,
int? height,
int? bitDepth,
int? videoBitrate,
- string videoProfile,
+ string? videoProfile,
VideoRangeType videoRangeType,
double? videoLevel,
float? videoFramerate,
@@ -456,7 +456,7 @@ namespace MediaBrowser.Model.Dlna
int? refFrames,
int? numVideoStreams,
int? numAudioStreams,
- string videoCodecTag,
+ string? videoCodecTag,
bool? isAvc)
{
foreach (var i in ResponseProfiles)
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index f6b882c3e..666e78795 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -135,7 +135,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- if (transcodingProfile != null)
+ if (transcodingProfile is not null)
{
if (!item.SupportsTranscoding)
{
@@ -179,15 +179,9 @@ namespace MediaBrowser.Model.Dlna
{
ValidateMediaOptions(options, true);
- var mediaSources = new List<MediaSourceInfo>();
- foreach (var mediaSourceInfo in options.MediaSources)
- {
- if (string.IsNullOrEmpty(options.MediaSourceId)
- || string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
- {
- mediaSources.Add(mediaSourceInfo);
- }
- }
+ var mediaSources = string.IsNullOrEmpty(options.MediaSourceId)
+ ? options.MediaSources
+ : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase));
var streams = new List<StreamInfo>();
foreach (var mediaSourceInfo in mediaSources)
@@ -216,7 +210,7 @@ namespace MediaBrowser.Model.Dlna
return streams.OrderBy(i =>
{
// Nothing beats direct playing a file
- if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource.Protocol == MediaProtocol.File)
+ if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource?.Protocol == MediaProtocol.File)
{
return 0;
}
@@ -235,7 +229,7 @@ namespace MediaBrowser.Model.Dlna
}
}).ThenBy(i =>
{
- switch (i.MediaSource.Protocol)
+ switch (i.MediaSource?.Protocol)
{
case MediaProtocol.File:
return 0;
@@ -246,7 +240,7 @@ namespace MediaBrowser.Model.Dlna
{
if (maxBitrate > 0)
{
- if (i.MediaSource.Bitrate.HasValue)
+ if (i.MediaSource?.Bitrate is not null)
{
return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate);
}
@@ -585,10 +579,10 @@ namespace MediaBrowser.Model.Dlna
MediaSource = item,
RunTimeTicks = item.RunTimeTicks,
Context = options.Context,
- DeviceProfile = options.Profile
+ DeviceProfile = options.Profile,
+ SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles)
};
- playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles);
var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null;
var audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
@@ -659,7 +653,8 @@ namespace MediaBrowser.Model.Dlna
if (audioStreamIndex.HasValue)
{
playlistItem.AudioStreamIndex = audioStreamIndex;
- playlistItem.AudioCodecs = new[] { item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec };
+ var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec;
+ playlistItem.AudioCodecs = audioCodec is null ? Array.Empty<string>() : new[] { audioCodec };
}
}
else if (directPlay == PlayMethod.DirectStream)
@@ -759,7 +754,7 @@ namespace MediaBrowser.Model.Dlna
{
// prefer direct copy profile
float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0;
- TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
+ TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
@@ -842,7 +837,7 @@ namespace MediaBrowser.Model.Dlna
if (videoStream is not null && videoStream.Level != 0)
{
- playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString());
+ playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString() ?? string.Empty);
}
// Prefer matching audio codecs, could do better here
@@ -871,7 +866,7 @@ namespace MediaBrowser.Model.Dlna
// Copy matching audio codec options
playlistItem.AudioSampleRate = audioStream.SampleRate;
- playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString());
+ playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString() ?? string.Empty);
if (!string.IsNullOrEmpty(audioStream.Profile))
{
@@ -880,7 +875,7 @@ namespace MediaBrowser.Model.Dlna
if (audioStream.Level != 0)
{
- playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString());
+ playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString() ?? string.Empty);
}
}
@@ -1318,7 +1313,7 @@ namespace MediaBrowser.Model.Dlna
var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions);
- if (audioStream?.IsExternal == true)
+ if (audioStream.IsExternal == true)
{
audioStreamFailureReasons |= TranscodeReason.AudioIsExternal;
}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 00543616d..fc146df30 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -1,9 +1,9 @@
-#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -34,9 +34,9 @@ namespace MediaBrowser.Model.Dlna
public DlnaProfileType MediaType { get; set; }
- public string Container { get; set; }
+ public string? Container { get; set; }
- public string SubProtocol { get; set; }
+ public string? SubProtocol { get; set; }
public long StartPositionTicks { get; set; }
@@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Dlna
public float? MaxFramerate { get; set; }
- public DeviceProfile DeviceProfile { get; set; }
+ public required DeviceProfile DeviceProfile { get; set; }
- public string DeviceProfileId { get; set; }
+ public string? DeviceProfileId { get; set; }
- public string DeviceId { get; set; }
+ public string? DeviceId { get; set; }
public long? RunTimeTicks { get; set; }
@@ -92,21 +92,21 @@ namespace MediaBrowser.Model.Dlna
public bool EstimateContentLength { get; set; }
- public MediaSourceInfo MediaSource { get; set; }
+ public MediaSourceInfo? MediaSource { get; set; }
public string[] SubtitleCodecs { get; set; }
public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
- public string SubtitleFormat { get; set; }
+ public string? SubtitleFormat { get; set; }
- public string PlaySessionId { get; set; }
+ public string? PlaySessionId { get; set; }
public TranscodeReason TranscodeReasons { get; set; }
public Dictionary<string, string> StreamOptions { get; private set; }
- public string MediaSourceId => MediaSource?.Id;
+ public string? MediaSourceId => MediaSource?.Id;
public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
&& PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
@@ -114,12 +114,12 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// Gets the audio stream that will be used.
/// </summary>
- public MediaStream TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
+ public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
/// <summary>
/// Gets the video stream that will be used.
/// </summary>
- public MediaStream TargetVideoStream => MediaSource?.VideoStream;
+ public MediaStream? TargetVideoStream => MediaSource?.VideoStream;
/// <summary>
/// Gets the audio sample rate that will be in the output stream.
@@ -259,7 +259,7 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// Gets the audio sample rate that will be in the output stream.
/// </summary>
- public string TargetVideoProfile
+ public string? TargetVideoProfile
{
get
{
@@ -307,7 +307,7 @@ namespace MediaBrowser.Model.Dlna
/// Gets the target video codec tag.
/// </summary>
/// <value>The target video codec tag.</value>
- public string TargetVideoCodecTag
+ public string? TargetVideoCodecTag
{
get
{
@@ -364,7 +364,7 @@ namespace MediaBrowser.Model.Dlna
{
var stream = TargetAudioStream;
- string inputCodec = stream?.Codec;
+ string? inputCodec = stream?.Codec;
if (IsDirectStream)
{
@@ -389,7 +389,7 @@ namespace MediaBrowser.Model.Dlna
{
var stream = TargetVideoStream;
- string inputCodec = stream?.Codec;
+ string? inputCodec = stream?.Codec;
if (IsDirectStream)
{
@@ -417,7 +417,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return MediaSource.Size;
+ return MediaSource?.Size;
}
if (RunTimeTicks.HasValue)
@@ -580,7 +580,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- public void SetOption(string qualifier, string name, string value)
+ public void SetOption(string? qualifier, string name, string value)
{
if (string.IsNullOrEmpty(qualifier))
{
@@ -597,7 +597,7 @@ namespace MediaBrowser.Model.Dlna
StreamOptions[name] = value;
}
- public string GetOption(string qualifier, string name)
+ public string? GetOption(string? qualifier, string name)
{
var value = GetOption(qualifier + "-" + name);
@@ -609,7 +609,7 @@ namespace MediaBrowser.Model.Dlna
return value;
}
- public string GetOption(string name)
+ public string? GetOption(string name)
{
if (StreamOptions.TryGetValue(name, out var value))
{
@@ -619,7 +619,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- public string ToUrl(string baseUrl, string accessToken)
+ public string ToUrl(string baseUrl, string? accessToken)
{
ArgumentException.ThrowIfNullOrEmpty(baseUrl);
@@ -686,7 +686,7 @@ namespace MediaBrowser.Model.Dlna
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
}
- private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string accessToken)
+ private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
{
var list = new List<NameValuePair>();
@@ -730,7 +730,7 @@ namespace MediaBrowser.Model.Dlna
list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
- string liveStreamId = item.MediaSource?.LiveStreamId;
+ string? liveStreamId = item.MediaSource?.LiveStreamId;
list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
@@ -772,7 +772,7 @@ namespace MediaBrowser.Model.Dlna
list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
}
- list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
+ list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
string.Empty :
@@ -816,13 +816,18 @@ namespace MediaBrowser.Model.Dlna
return list;
}
- public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
+ public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken)
{
return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
}
- public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+ public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken)
{
+ if (MediaSource is null)
+ {
+ return Enumerable.Empty<SubtitleStreamInfo>();
+ }
+
var list = new List<SubtitleStreamInfo>();
// HLS will preserve timestamps so we can just grab the full subtitle stream
@@ -856,27 +861,36 @@ namespace MediaBrowser.Model.Dlna
return list;
}
- private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks)
+ private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks)
{
if (enableAllProfiles)
{
foreach (var profile in DeviceProfile.SubtitleProfiles)
{
var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
-
- list.Add(info);
+ if (info is not null)
+ {
+ list.Add(info);
+ }
}
}
else
{
var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
-
- list.Add(info);
+ if (info is not null)
+ {
+ list.Add(info);
+ }
}
}
- private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
+ private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
{
+ if (MediaSource is null)
+ {
+ return null;
+ }
+
var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
var info = new SubtitleStreamInfo
{
@@ -920,7 +934,7 @@ namespace MediaBrowser.Model.Dlna
return info;
}
- public int? GetTargetVideoBitDepth(string codec)
+ public int? GetTargetVideoBitDepth(string? codec)
{
var value = GetOption(codec, "videobitdepth");
@@ -932,7 +946,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- public int? GetTargetAudioBitDepth(string codec)
+ public int? GetTargetAudioBitDepth(string? codec)
{
var value = GetOption(codec, "audiobitdepth");
@@ -944,7 +958,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- public double? GetTargetVideoLevel(string codec)
+ public double? GetTargetVideoLevel(string? codec)
{
var value = GetOption(codec, "level");
@@ -956,7 +970,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- public int? GetTargetRefFrames(string codec)
+ public int? GetTargetRefFrames(string? codec)
{
var value = GetOption(codec, "maxrefframes");
@@ -968,7 +982,7 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- public int? GetTargetAudioChannels(string codec)
+ public int? GetTargetAudioChannels(string? codec)
{
var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
@@ -988,7 +1002,7 @@ namespace MediaBrowser.Model.Dlna
private int? GetMediaStreamCount(MediaStreamType type, int limit)
{
- var count = MediaSource.GetStreamCount(type);
+ var count = MediaSource?.GetStreamCount(type);
if (count.HasValue)
{
diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
index 68a5c2534..1bb24112e 100644
--- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
+++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
@@ -24,4 +24,21 @@ public static class ImageFormatExtensions
ImageFormat.Webp => "image/webp",
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
};
+
+ /// <summary>
+ /// Returns the correct extension for this <see cref="ImageFormat" />.
+ /// </summary>
+ /// <param name="format">This <see cref="ImageFormat" />.</param>
+ /// <exception cref="InvalidEnumArgumentException">The <paramref name="format"/> is an invalid enumeration value.</exception>
+ /// <returns>The correct extension for this <see cref="ImageFormat" />.</returns>
+ public static string GetExtension(this ImageFormat format)
+ => format switch
+ {
+ ImageFormat.Bmp => ".bmp",
+ ImageFormat.Gif => ".gif",
+ ImageFormat.Jpg => ".jpg",
+ ImageFormat.Png => ".png",
+ ImageFormat.Webp => ".webp",
+ _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
+ };
}
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 8fab1ca6d..287966dd0 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
@@ -569,6 +570,12 @@ namespace MediaBrowser.Model.Dto
public List<ChapterInfo> Chapters { get; set; }
/// <summary>
+ /// Gets or sets the trickplay manifest.
+ /// </summary>
+ /// <value>The trickplay manifest.</value>
+ public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; }
+
+ /// <summary>
/// Gets or sets the type of the location.
/// </summary>
/// <value>The type of the location.</value>
diff --git a/MediaBrowser.Model/Entities/ChapterInfo.cs b/MediaBrowser.Model/Entities/ChapterInfo.cs
index 45554c3dc..d6b905651 100644
--- a/MediaBrowser.Model/Entities/ChapterInfo.cs
+++ b/MediaBrowser.Model/Entities/ChapterInfo.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -20,16 +19,16 @@ namespace MediaBrowser.Model.Entities
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the image path.
/// </summary>
/// <value>The image path.</value>
- public string ImagePath { get; set; }
+ public string? ImagePath { get; set; }
public DateTime ImageDateModified { get; set; }
- public string ImageTag { get; set; }
+ public string? ImageTag { get; set; }
}
}
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index 786b20e9e..ec381d423 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO
/// </summary>
public interface IFileSystem
{
- void AddShortcutHandler(IShortcutHandler handler);
-
/// <summary>
/// Determines whether the specified filename is shortcut.
/// </summary>
@@ -117,13 +115,6 @@ namespace MediaBrowser.Model.IO
bool ContainsSubPath(string parentPath, string path);
/// <summary>
- /// Normalizes the path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>System.String.</returns>
- string NormalizePath(string path);
-
- /// <summary>
/// Gets the file name without extension.
/// </summary>
/// <param name="info">The information.</param>
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 58ba83a35..75c5bc6f0 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -48,8 +48,12 @@
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@@ -58,6 +62,7 @@
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
+
<ItemGroup>
<ProjectReference Include="../Jellyfin.Data/Jellyfin.Data.csproj" />
<ProjectReference Include="../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" />
diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs
index 985b16c6e..e9fcd6797 100644
--- a/MediaBrowser.Model/Net/IPData.cs
+++ b/MediaBrowser.Model/Net/IPData.cs
@@ -48,6 +48,11 @@ public class IPData
public int Index { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether the network supports multicast.
+ /// </summary>
+ public bool SupportsMulticast { get; set; } = false;
+
+ /// <summary>
/// Gets or sets the interface name.
/// </summary>
public string Name { get; set; }
diff --git a/MediaBrowser.Model/Net/PublishedServerUriOverride.cs b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs
new file mode 100644
index 000000000..476d1ba38
--- /dev/null
+++ b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs
@@ -0,0 +1,42 @@
+namespace MediaBrowser.Model.Net;
+
+/// <summary>
+/// Class holding information for a published server URI override.
+/// </summary>
+public class PublishedServerUriOverride
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PublishedServerUriOverride"/> class.
+ /// </summary>
+ /// <param name="data">The <see cref="IPData"/>.</param>
+ /// <param name="overrideUri">The override.</param>
+ /// <param name="internalOverride">A value indicating whether the override is for internal requests.</param>
+ /// <param name="externalOverride">A value indicating whether the override is for external requests.</param>
+ public PublishedServerUriOverride(IPData data, string overrideUri, bool internalOverride, bool externalOverride)
+ {
+ Data = data;
+ OverrideUri = overrideUri;
+ IsInternalOverride = internalOverride;
+ IsExternalOverride = externalOverride;
+ }
+
+ /// <summary>
+ /// Gets or sets the object's IP address.
+ /// </summary>
+ public IPData Data { get; set; }
+
+ /// <summary>
+ /// Gets or sets the override URI.
+ /// </summary>
+ public string OverrideUri { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the override should be applied to internal requests.
+ /// </summary>
+ public bool IsInternalOverride { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the override should be applied to external requests.
+ /// </summary>
+ public bool IsExternalOverride { get; set; }
+}
diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs
index 6fa1d778a..242a1c6e9 100644
--- a/MediaBrowser.Model/Querying/ItemFields.cs
+++ b/MediaBrowser.Model/Querying/ItemFields.cs
@@ -34,6 +34,11 @@ namespace MediaBrowser.Model.Querying
/// </summary>
Chapters,
+ /// <summary>
+ /// The trickplay manifest.
+ /// </summary>
+ Trickplay,
+
ChildCount,
/// <summary>
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 0fb996df9..35353e6fa 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Querying
EnableTotalRecordCount = true;
DisableFirstEpisode = false;
NextUpDateCutoff = DateTime.MinValue;
+ EnableResumable = false;
EnableRewatching = false;
}
@@ -84,6 +85,11 @@ namespace MediaBrowser.Model.Querying
public DateTime NextUpDateCutoff { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether to include resumable episodes as next up.
+ /// </summary>
+ public bool EnableResumable { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether getting rewatching next up list.
/// </summary>
public bool EnableRewatching { get; set; }
diff --git a/MediaBrowser.Model/System/CastReceiverApplication.cs b/MediaBrowser.Model/System/CastReceiverApplication.cs
new file mode 100644
index 000000000..6a49a5cac
--- /dev/null
+++ b/MediaBrowser.Model/System/CastReceiverApplication.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.System;
+
+/// <summary>
+/// The cast receiver application model.
+/// </summary>
+public class CastReceiverApplication
+{
+ /// <summary>
+ /// Gets or sets the cast receiver application id.
+ /// </summary>
+ public required string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cast receiver application name.
+ /// </summary>
+ public required string Name { get; set; }
+}
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index bd0099af7..aa7c03ebd 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using System.Runtime.InteropServices;
using MediaBrowser.Model.Updates;
@@ -84,7 +85,8 @@ namespace MediaBrowser.Model.System
[Obsolete("This is always true")]
public bool CanSelfRestart { get; set; } = true;
- public bool CanLaunchWebBrowser { get; set; }
+ [Obsolete("This is always false")]
+ public bool CanLaunchWebBrowser { get; set; } = false;
/// <summary>
/// Gets or sets the program data path.
@@ -129,6 +131,11 @@ namespace MediaBrowser.Model.System
public string TranscodingTempPath { get; set; }
/// <summary>
+ /// Gets or sets the list of cast receiver applications.
+ /// </summary>
+ public IReadOnlyList<CastReceiverApplication> CastReceiverApplications { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance has update available.
/// </summary>
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 8354c60ef..f5aff07db 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Users
{
IsHidden = true;
EnableCollectionManagement = false;
+ EnableSubtitleManagement = false;
EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty<string>();
@@ -84,6 +85,13 @@ namespace MediaBrowser.Model.Users
public bool EnableCollectionManagement { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether this instance can manage subtitles.
+ /// </summary>
+ /// <value><c>true</c> if this instance is allowed; otherwise, <c>false</c>.</value>
+ [DefaultValue(false)]
+ public bool EnableSubtitleManagement { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance is disabled.
/// </summary>
/// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index e7c2cd255..d82716831 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -263,7 +263,11 @@ namespace MediaBrowser.Providers.Manager
var fileStreamOptions = AsyncFile.WriteOptions;
fileStreamOptions.Mode = FileMode.Create;
- fileStreamOptions.PreallocationSize = source.Length;
+ if (source.CanSeek)
+ {
+ fileStreamOptions.PreallocationSize = source.Length;
+ }
+
var fs = new FileStream(path, fileStreamOptions);
await using (fs.ConfigureAwait(false))
{
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 834ef29f5..e336c8825 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -12,7 +12,6 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
@@ -720,7 +719,7 @@ namespace MediaBrowser.Providers.Manager
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
}
- MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true);
+ MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true);
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
// Only one local provider allowed per item
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 5cb28402e..4ba884418 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -765,10 +765,12 @@ namespace MediaBrowser.Providers.Manager
{
try
{
- var results = await GetSearchResults(provider, searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false);
+ var results = await provider.GetSearchResults(searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false);
foreach (var result in results)
{
+ result.SearchProviderName = provider.Name;
+
var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.GetProviderId(p.Key), p.Value, StringComparison.OrdinalIgnoreCase)));
if (existingMatch is null)
@@ -800,22 +802,6 @@ namespace MediaBrowser.Providers.Manager
return resultList;
}
- private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(
- IRemoteSearchProvider<TLookupType> provider,
- TLookupType searchInfo,
- CancellationToken cancellationToken)
- where TLookupType : ItemLookupInfo
- {
- var results = await provider.GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
-
- foreach (var item in results)
- {
- item.SearchProviderName = provider.Name;
- }
-
- return results;
- }
-
private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
{
return _externalIds.Where(i =>
@@ -957,6 +943,12 @@ namespace MediaBrowser.Providers.Manager
/// <inheritdoc/>
public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
{
+ ArgumentNullException.ThrowIfNull(itemId);
+ if (itemId.Equals(default))
+ {
+ throw new ArgumentException("Guid can't be empty", nameof(itemId));
+ }
+
if (_disposed)
{
return;
@@ -1104,13 +1096,13 @@ namespace MediaBrowser.Providers.Manager
return;
}
- if (!_disposeCancellationTokenSource.IsCancellationRequested)
- {
- _disposeCancellationTokenSource.Cancel();
- }
-
if (disposing)
{
+ if (!_disposeCancellationTokenSource.IsCancellationRequested)
+ {
+ _disposeCancellationTokenSource.Cancel();
+ }
+
_disposeCancellationTokenSource.Dispose();
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 6a40833d7..8471f6fa1 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
@@ -33,8 +33,12 @@
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
- <!-- Code Analyzers-->
+ <!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 9bcb1c39b..d81704227 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaSourceManager = mediaSourceManager;
}
- [GeneratedRegex("I:\\s+(.*?)\\s+LUFS")]
+ [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
private static partial Regex LUFSRegex();
/// <summary>
@@ -107,7 +107,6 @@ namespace MediaBrowser.Providers.MediaInfo
if (libraryOptions.EnableLUFSScan)
{
- string output;
using (var process = new Process()
{
StartInfo = new ProcessStartInfo
@@ -130,7 +129,8 @@ namespace MediaBrowser.Providers.MediaInfo
throw;
}
- output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+ using var reader = process.StandardError;
+ var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
MatchCollection split = LUFSRegex().Matches(output);
diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
index f58f5f7a3..0bfee07fd 100644
--- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
@@ -177,9 +177,11 @@ namespace MediaBrowser.Providers.MediaInfo
var format = imageStream.Codec switch
{
+ "bmp" => ImageFormat.Bmp,
+ "gif" => ImageFormat.Gif,
"mjpeg" => ImageFormat.Jpg,
"png" => ImageFormat.Png,
- "gif" => ImageFormat.Gif,
+ "webp" => ImageFormat.Webp,
_ => ImageFormat.Jpg
};
@@ -202,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo
? Path.GetExtension(attachmentStream.FileName)
: MimeTypes.ToExtension(attachmentStream.MimeType);
- if (string.IsNullOrEmpty(extension))
- {
- extension = ".jpg";
- }
-
ImageFormat format = extension switch
{
".bmp" => ImageFormat.Bmp,
".gif" => ImageFormat.Gif,
- ".jpg" => ImageFormat.Jpg,
".png" => ImageFormat.Png,
".webp" => ImageFormat.Webp,
_ => ImageFormat.Jpg
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 213639371..35ea04d21 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -1,11 +1,8 @@
-#nullable disable
-
#pragma warning disable CA1068, CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -83,9 +80,9 @@ namespace MediaBrowser.Providers.MediaInfo
CancellationToken cancellationToken)
where T : Video
{
- BlurayDiscInfo blurayDiscInfo = null;
+ BlurayDiscInfo? blurayDiscInfo = null;
- Model.MediaInfo.MediaInfo mediaInfoResult = null;
+ Model.MediaInfo.MediaInfo? mediaInfoResult = null;
if (!item.IsShortcut || options.EnableRemoteContentProbe)
{
@@ -131,7 +128,7 @@ namespace MediaBrowser.Providers.MediaInfo
var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path);
// Return if no playable .m2ts files are found
- if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0)
+ if (blurayDiscInfo is null || blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0)
{
_logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe.");
return ItemUpdateType.MetadataImport;
@@ -192,16 +189,14 @@ namespace MediaBrowser.Providers.MediaInfo
protected async Task Fetch(
Video video,
CancellationToken cancellationToken,
- Model.MediaInfo.MediaInfo mediaInfo,
- BlurayDiscInfo blurayInfo,
+ Model.MediaInfo.MediaInfo? mediaInfo,
+ BlurayDiscInfo? blurayInfo,
MetadataRefreshOptions options)
{
- List<MediaStream> mediaStreams;
+ List<MediaStream> mediaStreams = new List<MediaStream>();
IReadOnlyList<MediaAttachment> mediaAttachments;
ChapterInfo[] chapters;
- mediaStreams = new List<MediaStream>();
-
// Add external streams before adding the streams from the file to preserve stream IDs on remote videos
await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
@@ -221,18 +216,6 @@ namespace MediaBrowser.Providers.MediaInfo
video.TotalBitrate = mediaInfo.Bitrate;
video.RunTimeTicks = mediaInfo.RunTimeTicks;
video.Size = mediaInfo.Size;
-
- if (video.VideoType == VideoType.VideoFile)
- {
- var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.');
-
- video.Container = extension;
- }
- else
- {
- video.Container = null;
- }
-
video.Container = mediaInfo.Container;
chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
@@ -243,8 +226,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
else
{
- var currentMediaStreams = video.GetMediaStreams();
- foreach (var mediaStream in currentMediaStreams)
+ foreach (var mediaStream in video.GetMediaStreams())
{
if (!mediaStream.IsExternal)
{
@@ -295,8 +277,8 @@ namespace MediaBrowser.Providers.MediaInfo
_itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
}
- if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
- options.MetadataRefreshMode == MetadataRefreshMode.Default)
+ if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh
+ || options.MetadataRefreshMode == MetadataRefreshMode.Default)
{
if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
{
@@ -321,11 +303,11 @@ namespace MediaBrowser.Providers.MediaInfo
{
for (int i = 0; i < chapters.Length; i++)
{
- string name = chapters[i].Name;
+ string? name = chapters[i].Name;
// Check if the name is empty and/or if the name is a time
// Some ripping programs do that.
- if (string.IsNullOrWhiteSpace(name) ||
- TimeSpan.TryParse(name, out _))
+ if (string.IsNullOrWhiteSpace(name)
+ || TimeSpan.TryParse(name, out _))
{
chapters[i].Name = string.Format(
CultureInfo.InvariantCulture,
@@ -384,23 +366,18 @@ namespace MediaBrowser.Providers.MediaInfo
// Use the ffprobe values if these are empty
if (videoStream is not null)
{
- videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
- videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
- videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
+ videoStream.BitRate = videoStream.BitRate.GetValueOrDefault() == 0 ? currentBitRate : videoStream.BitRate;
+ videoStream.Width = videoStream.Width.GetValueOrDefault() == 0 ? currentWidth : videoStream.Width;
+ videoStream.Height = videoStream.Height.GetValueOrDefault() == 0 ? currentHeight : videoStream.Height;
}
}
- private bool IsEmpty(int? num)
- {
- return !num.HasValue || num.Value == 0;
- }
-
/// <summary>
/// Gets information about the longest playlist on a bdrom.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoStream.</returns>
- private BlurayDiscInfo GetBDInfo(string path)
+ private BlurayDiscInfo? GetBDInfo(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
@@ -527,32 +504,29 @@ namespace MediaBrowser.Providers.MediaInfo
private void FetchPeople(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions options)
{
- var replaceData = options.ReplaceAllMetadata;
+ if (video.IsLocked
+ || video.LockedFields.Contains(MetadataField.Cast)
+ || data.People.Length == 0)
+ {
+ return;
+ }
- if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Cast))
+ if (options.ReplaceAllMetadata || _libraryManager.GetPeople(video).Count == 0)
{
- if (replaceData || _libraryManager.GetPeople(video).Count == 0)
- {
- var people = new List<PersonInfo>();
+ var people = new List<PersonInfo>();
- foreach (var person in data.People)
+ foreach (var person in data.People)
+ {
+ PeopleHelper.AddPerson(people, new PersonInfo
{
- PeopleHelper.AddPerson(people, new PersonInfo
- {
- Name = person.Name,
- Type = person.Type,
- Role = person.Role
- });
- }
-
- _libraryManager.UpdatePeople(video, people);
+ Name = person.Name,
+ Type = person.Type,
+ Role = person.Role
+ });
}
- }
- }
- private SubtitleOptions GetOptions()
- {
- return _config.GetConfiguration<SubtitleOptions>("subtitles");
+ _libraryManager.UpdatePeople(video, people);
+ }
}
/// <summary>
@@ -575,7 +549,7 @@ namespace MediaBrowser.Providers.MediaInfo
var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
- var subtitleOptions = GetOptions();
+ var subtitleOptions = _config.GetConfiguration<SubtitleOptions>("subtitles");
var libraryOptions = _libraryManager.GetLibraryOptions(video);
@@ -659,9 +633,9 @@ namespace MediaBrowser.Providers.MediaInfo
/// </summary>
/// <param name="video">The video.</param>
/// <returns>An array of dummy chapters.</returns>
- private ChapterInfo[] CreateDummyChapters(Video video)
+ internal ChapterInfo[] CreateDummyChapters(Video video)
{
- var runtime = video.RunTimeTicks ?? 0;
+ var runtime = video.RunTimeTicks.GetValueOrDefault();
// Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted.
if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks)
@@ -671,30 +645,30 @@ namespace MediaBrowser.Providers.MediaInfo
CultureInfo.InvariantCulture,
"{0} has an invalid runtime of {1} minutes",
video.Name,
- TimeSpan.FromTicks(runtime).Minutes));
+ TimeSpan.FromTicks(runtime).TotalMinutes));
}
long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
- if (runtime > dummyChapterDuration)
+ if (runtime <= dummyChapterDuration)
{
- int chapterCount = (int)(runtime / dummyChapterDuration);
- var chapters = new ChapterInfo[chapterCount];
+ return Array.Empty<ChapterInfo>();
+ }
- long currentChapterTicks = 0;
- for (int i = 0; i < chapterCount; i++)
- {
- chapters[i] = new ChapterInfo
- {
- StartPositionTicks = currentChapterTicks
- };
+ int chapterCount = (int)(runtime / dummyChapterDuration);
+ var chapters = new ChapterInfo[chapterCount];
- currentChapterTicks += dummyChapterDuration;
- }
+ long currentChapterTicks = 0;
+ for (int i = 0; i < chapterCount; i++)
+ {
+ chapters[i] = new ChapterInfo
+ {
+ StartPositionTicks = currentChapterTicks
+ };
- return chapters;
+ currentChapterTicks += dummyChapterDuration;
}
- return Array.Empty<ChapterInfo>();
+ return chapters;
}
}
}
diff --git a/MediaBrowser.Providers/Movies/ImdbExternalId.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
index d00f37db5..a8d74aa0b 100644
--- a/MediaBrowser.Providers/Movies/ImdbExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.imdb.com/title/{0}";
+ public string UrlFormatString => "https://www.imdb.com/title/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
index 1bb5e1ea8..8151ab471 100644
--- a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.imdb.com/name/{0}";
+ public string UrlFormatString => "https://www.imdb.com/name/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Person;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
index 3a400575b..138cfef19 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+ public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
index 55e2474a5..daad9706c 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
@@ -176,17 +176,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
- var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (stream.ConfigureAwait(false))
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ var fs = new FileStream(path, fileStreamOptions);
+ await using (fs.ConfigureAwait(false))
{
- var fileStreamOptions = AsyncFile.WriteOptions;
- fileStreamOptions.Mode = FileMode.Create;
- fileStreamOptions.PreallocationSize = stream.Length;
- var xmlFileStream = new FileStream(path, fileStreamOptions);
- await using (xmlFileStream.ConfigureAwait(false))
- {
- await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
- }
+ await response.Content.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
index b9e57eb26..8aceb48c0 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+ public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index f3385b3a9..92742b1aa 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -154,20 +154,15 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (stream.ConfigureAwait(false))
+ var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ var xmlFileStream = new FileStream(path, fileStreamOptions);
+ await using (xmlFileStream.ConfigureAwait(false))
{
- var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- var fileStreamOptions = AsyncFile.WriteOptions;
- fileStreamOptions.Mode = FileMode.Create;
- fileStreamOptions.PreallocationSize = stream.Length;
- var xmlFileStream = new FileStream(path, fileStreamOptions);
- await using (xmlFileStream.ConfigureAwait(false))
- {
- await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
- }
+ await response.Content.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
index f8f6253ff..014481da2 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+ public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
index fd598c918..787539104 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
- public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+ public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
index f7850781e..825fe32fa 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzAlbumArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
index a9d4472e7..b7d53984c 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzAlbumExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
index b89e67270..b3f001618 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
index fdaa5574f..a0a922293 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzOtherArtistExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
index 0baab9955..47b6d6963 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzReleaseGroupExternalId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
index 5c974c411..cb4345660 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
@@ -20,7 +20,7 @@ public class MusicBrainzTrackId : IExternalId
public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
/// <inheritdoc />
- public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
+ public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index e4bb4eaea..e84f1359b 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -8,6 +8,7 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading;
@@ -137,31 +138,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var url = OmdbProvider.GetOmdbUrl(urlQuery.ToString());
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
- var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (stream.ConfigureAwait(false))
+ if (isSearch)
{
- if (isSearch)
+ var searchResultList = await response.Content.ReadFromJsonAsync<SearchResultList>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (searchResultList?.Search is not null)
{
- var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (searchResultList?.Search is not null)
+ var resultCount = searchResultList.Search.Count;
+ var result = new RemoteSearchResult[resultCount];
+ for (var i = 0; i < resultCount; i++)
{
- var resultCount = searchResultList.Search.Count;
- var result = new RemoteSearchResult[resultCount];
- for (var i = 0; i < resultCount; i++)
- {
- result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd);
- }
-
- return result;
+ result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd);
}
+
+ return result;
}
- else
+ }
+ else
+ {
+ var result = await response.Content.ReadFromJsonAsync<SearchResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase))
{
- var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase))
- {
- return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) };
- }
+ return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) };
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
index 0e768bb83..d453a4ff4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
/// <inheritdoc />
- public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
index 38d2c5c69..6d6032e8f 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
/// <inheritdoc />
- public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
index 027399aec..d26a70028 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
index df04cb2e7..5f2d7909a 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
/// <inheritdoc />
- public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 9016e5de0..e01c0f483 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -213,11 +214,10 @@ namespace MediaBrowser.Providers.TV
{
// Null season numbers will have a 'dummy' season created because seasons are always required.
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
- string? seasonName = null;
- if (seasonNumber.HasValue && seasonNames.TryGetValue(seasonNumber.Value, out var tmp))
+ if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName))
{
- seasonName = tmp;
+ seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
}
if (existingSeason is null)
@@ -225,9 +225,9 @@ namespace MediaBrowser.Providers.TV
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
series.AddChild(season);
}
- else
+ else if (!string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal))
{
- existingSeason.Name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber);
+ existingSeason.Name = seasonName;
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
}
@@ -247,7 +247,6 @@ namespace MediaBrowser.Providers.TV
int? seasonNumber,
CancellationToken cancellationToken)
{
- seasonName = GetValidSeasonNameForSeries(series, seasonName, seasonNumber);
Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
var season = new Season
diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
index 087e4036a..3cb18e424 100644
--- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.TV
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string? UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
+ public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Series;
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
new file mode 100644
index 000000000..69f10b43b
--- /dev/null
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+using TagLib.Ape;
+
+namespace MediaBrowser.Providers.Trickplay;
+
+/// <summary>
+/// Class TrickplayImagesTask.
+/// </summary>
+public class TrickplayImagesTask : IScheduledTask
+{
+ private const int QueryPageLimit = 100;
+
+ private readonly ILogger<TrickplayImagesTask> _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly ITrickplayManager _trickplayManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayImagesTask"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="localization">The localization manager.</param>
+ /// <param name="trickplayManager">The trickplay manager.</param>
+ public TrickplayImagesTask(
+ ILogger<TrickplayImagesTask> logger,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ ITrickplayManager trickplayManager)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _localization = localization;
+ _trickplayManager = trickplayManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription");
+
+ /// <inheritdoc />
+ public string Key => "RefreshTrickplayImages";
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[]
+ {
+ new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerDaily,
+ TimeOfDayTicks = TimeSpan.FromHours(3).Ticks
+ }
+ };
+ }
+
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var query = new InternalItemsQuery
+ {
+ MediaTypes = new[] { MediaType.Video },
+ SourceTypes = new[] { SourceType.Library },
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true,
+ Limit = QueryPageLimit
+ };
+
+ var numberOfVideos = _libraryManager.GetCount(query);
+
+ var startIndex = 0;
+ var numComplete = 0;
+
+ while (startIndex < numberOfVideos)
+ {
+ query.StartIndex = startIndex;
+ var videos = _libraryManager.GetItemList(query).OfType<Video>();
+
+ foreach (var video in videos)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ await _trickplayManager.RefreshTrickplayDataAsync(video, false, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating trickplay files for {ItemName}", video.Name);
+ }
+
+ numComplete++;
+ progress.Report(100d * numComplete / numberOfVideos);
+ }
+
+ startIndex += QueryPageLimit;
+ }
+
+ progress.Report(100);
+ }
+}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
new file mode 100644
index 000000000..f6dcde4f6
--- /dev/null
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -0,0 +1,121 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Providers.Trickplay;
+
+/// <summary>
+/// Class TrickplayProvider. Provides images and metadata for trickplay
+/// scrubbing previews.
+/// </summary>
+public class TrickplayProvider : ICustomMetadataProvider<Episode>,
+ ICustomMetadataProvider<MusicVideo>,
+ ICustomMetadataProvider<Movie>,
+ ICustomMetadataProvider<Trailer>,
+ ICustomMetadataProvider<Video>,
+ IHasItemChangeMonitor,
+ IHasOrder,
+ IForcedProvider
+{
+ private readonly IServerConfigurationManager _config;
+ private readonly ITrickplayManager _trickplayManager;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrickplayProvider"/> class.
+ /// </summary>
+ /// <param name="config">The configuration manager.</param>
+ /// <param name="trickplayManager">The trickplay manager.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ public TrickplayProvider(
+ IServerConfigurationManager config,
+ ITrickplayManager trickplayManager,
+ ILibraryManager libraryManager)
+ {
+ _config = config;
+ _trickplayManager = trickplayManager;
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Trickplay Provider";
+
+ /// <inheritdoc />
+ public int Order => 100;
+
+ /// <inheritdoc />
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ if (item.IsFileProtocol)
+ {
+ var file = directoryService.GetFile(item.Path);
+ if (file is not null && item.DateModified != file.LastWriteTimeUtc)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ private async Task<ItemUpdateType> FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
+ bool replace = options.ReplaceAllImages;
+
+ if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
+ {
+ return ItemUpdateType.None;
+ }
+
+ if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking)
+ {
+ await _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ _ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
+ }
+
+ // The core doesn't need to trigger any save operations over this
+ return ItemUpdateType.None;
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 807234915..d7e34fd22 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -22,6 +22,10 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 5b68924ac..70e5b66c1 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Common.Providers;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -159,7 +160,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
// Find last closing Tag
// Need to do this in two steps to account for random > characters after the closing xml
- var index = xml.LastIndexOf(@"</", StringComparison.Ordinal);
+ var index = xml.LastIndexOf("</", StringComparison.Ordinal);
// If closing tag exists, move to end of Tag
if (index != -1)
@@ -261,158 +262,84 @@ namespace MediaBrowser.XbmcMetadata.Parsers
protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult)
{
var item = itemResult.Item;
-
var nfoConfiguration = _config.GetNfoConfiguration();
- UserItemData? userData = null;
+ UserItemData? userData;
switch (reader.Name)
{
- // DateCreated
case "dateadded":
+ if (reader.TryReadDateTime(out var dateCreated))
{
- var val = reader.ReadElementContentAsString();
-
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
- {
- item.DateCreated = added;
- }
- else
- {
- Logger.LogWarning("Invalid Added value found: {Value}", val);
- }
-
- break;
+ item.DateCreated = dateCreated;
}
+ break;
case "originaltitle":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrEmpty(val))
- {
- item.OriginalTitle = val;
- }
-
- break;
- }
-
+ item.OriginalTitle = reader.ReadNormalizedString();
+ break;
case "name":
case "title":
case "localtitle":
- item.Name = reader.ReadElementContentAsString();
+ item.Name = reader.ReadNormalizedString();
break;
-
case "sortname":
- item.SortName = reader.ReadElementContentAsString();
+ item.SortName = reader.ReadNormalizedString();
break;
-
case "criticrating":
+ var criticRatingText = reader.ReadElementContentAsString();
+ if (float.TryParse(criticRatingText, CultureInfo.InvariantCulture, out var value))
{
- var text = reader.ReadElementContentAsString();
-
- if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
- {
- item.CriticRating = value;
- }
-
- break;
+ item.CriticRating = value;
}
+ break;
case "sorttitle":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.ForcedSortName = val;
- }
-
- break;
- }
-
+ item.ForcedSortName = reader.ReadNormalizedString();
+ break;
case "biography":
case "plot":
case "review":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.Overview = val;
- }
-
- break;
- }
-
+ item.Overview = reader.ReadNormalizedString();
+ break;
case "language":
- {
- var val = reader.ReadElementContentAsString();
-
- item.PreferredMetadataLanguage = val;
-
- break;
- }
-
+ item.PreferredMetadataLanguage = reader.ReadNormalizedString();
+ break;
case "watched":
+ var played = reader.ReadElementContentAsBoolean();
+ if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
{
- var val = reader.ReadElementContentAsBoolean();
-
- if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
- {
- var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
- userData = _userDataManager.GetUserData(user, item);
- userData.Played = val;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
- }
-
- break;
+ var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
+ userData = _userDataManager.GetUserData(user, item);
+ userData.Played = played;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
+ break;
case "playcount":
+ if (reader.TryReadInt(out var count)
+ && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId))
{
- var val = reader.ReadElementContentAsString();
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count)
- && Guid.TryParse(nfoConfiguration.UserId, out var guid))
- {
- var user = _userManager.GetUserById(guid);
- userData = _userDataManager.GetUserData(user, item);
- userData.PlayCount = count;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
- }
-
- break;
+ var user = _userManager.GetUserById(playCountUserId);
+ userData = _userDataManager.GetUserData(user, item);
+ userData.PlayCount = count;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
+ break;
case "lastplayed":
+ if (reader.TryReadDateTime(out var lastPlayed)
+ && Guid.TryParse(nfoConfiguration.UserId, out var lastPlayedUserId))
{
- var val = reader.ReadElementContentAsString();
- if (Guid.TryParse(nfoConfiguration.UserId, out var guid))
- {
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
- {
- var user = _userManager.GetUserById(guid);
- userData = _userDataManager.GetUserData(user, item);
- userData.LastPlayedDate = added;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
- }
- else
- {
- Logger.LogWarning("Invalid lastplayed value found: {Value}", val);
- }
- }
-
- break;
+ var user = _userManager.GetUserById(lastPlayedUserId);
+ userData = _userDataManager.GetUserData(user, item);
+ userData.LastPlayedDate = lastPlayed;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
+ break;
case "countrycode":
- {
- var val = reader.ReadElementContentAsString();
-
- item.PreferredMetadataCountryCode = val;
-
- break;
- }
-
+ item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
+ break;
case "lockedfields":
{
var val = reader.ReadElementContentAsString();
@@ -434,9 +361,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "tagline":
- item.Tagline = reader.ReadElementContentAsString();
+ item.Tagline = reader.ReadNormalizedString();
break;
-
case "country":
{
var val = reader.ReadElementContentAsString();
@@ -453,94 +379,45 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "mpaa":
- {
- var rating = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(rating))
- {
- item.OfficialRating = rating;
- }
-
- break;
- }
-
+ item.OfficialRating = reader.ReadNormalizedString();
+ break;
case "customrating":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.CustomRating = val;
- }
-
- break;
- }
-
+ item.CustomRating = reader.ReadNormalizedString();
+ break;
case "runtime":
+ var runtimeText = reader.ReadElementContentAsString();
+ if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
- var text = reader.ReadElementContentAsString();
-
- if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
- {
- item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
- }
-
- break;
+ item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
+ break;
case "aspectratio":
+ var aspectRatio = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val)
- && item is IHasAspectRatio hasAspectRatio)
- {
- hasAspectRatio.AspectRatio = val;
- }
-
- break;
+ hasAspectRatio.AspectRatio = aspectRatio;
}
+ break;
case "lockdata":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- }
-
+ item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
+ break;
case "studio":
+ var studio = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(studio))
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.AddStudio(val);
- }
-
- break;
+ item.AddStudio(studio);
}
+ break;
case "director":
+ foreach (var director in reader.GetPersonArray(PersonKind.Director))
{
- var val = reader.ReadElementContentAsString();
- foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
- {
- if (string.IsNullOrWhiteSpace(p.Name))
- {
- continue;
- }
-
- itemResult.AddPerson(p);
- }
-
- break;
+ itemResult.AddPerson(director);
}
+ break;
case "credits":
{
var val = reader.ReadElementContentAsString();
@@ -565,141 +442,76 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "writer":
+ foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
{
- var val = reader.ReadElementContentAsString();
- foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
- {
- if (string.IsNullOrWhiteSpace(p.Name))
- {
- continue;
- }
-
- itemResult.AddPerson(p);
- }
-
- break;
+ itemResult.AddPerson(writer);
}
+ break;
case "actor":
+ var person = reader.GetPersonFromXmlNode();
+ if (person is not null)
{
- if (!reader.IsEmptyElement)
- {
- using (var subtree = reader.ReadSubtree())
- {
- var person = GetPersonFromXmlNode(subtree);
-
- if (!string.IsNullOrWhiteSpace(person.Name))
- {
- itemResult.AddPerson(person);
- }
- }
- }
- else
- {
- reader.Read();
- }
-
- break;
+ itemResult.AddPerson(person);
}
+ break;
case "trailer":
+ var trailer = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(trailer))
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- val = val.Replace("plugin://plugin.video.youtube/?action=play_video&videoid=", BaseNfoSaver.YouTubeWatchUrl, StringComparison.OrdinalIgnoreCase);
-
- item.AddTrailerUrl(val);
- }
-
- break;
+ item.AddTrailerUrl(trailer.Replace(
+ "plugin://plugin.video.youtube/?action=play_video&videoid=",
+ BaseNfoSaver.YouTubeWatchUrl,
+ StringComparison.OrdinalIgnoreCase));
}
+ break;
case "displayorder":
+ var displayOrder = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
{
- var val = reader.ReadElementContentAsString();
-
- if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrWhiteSpace(val))
- {
- hasDisplayOrder.DisplayOrder = val;
- }
-
- break;
+ hasDisplayOrder.DisplayOrder = displayOrder;
}
+ break;
case "year":
+ if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
{
- var val = reader.ReadElementContentAsString();
-
- if (int.TryParse(val, out var productionYear) && productionYear > 1850)
- {
- item.ProductionYear = productionYear;
- }
-
- break;
+ item.ProductionYear = productionYear;
}
+ break;
case "rating":
+ var rating = reader.ReadElementContentAsString().Replace(',', '.');
+ // All external meta is saving this as '.' for decimal I believe...but just to be sure
+ if (float.TryParse(rating, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var communityRating))
{
- var rating = reader.ReadElementContentAsString();
-
- // All external meta is saving this as '.' for decimal I believe...but just to be sure
- if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
- {
- item.CommunityRating = val;
- }
-
- break;
+ item.CommunityRating = communityRating;
}
+ break;
case "ratings":
- {
- if (!reader.IsEmptyElement)
- {
- using var subtree = reader.ReadSubtree();
- FetchFromRatingsNode(subtree, item);
- }
- else
- {
- reader.Read();
- }
-
- break;
- }
-
+ FetchFromRatingsNode(reader, item);
+ break;
case "aired":
case "formed":
case "premiered":
case "releasedate":
+ if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
{
- var formatString = nfoConfiguration.ReleaseDateFormat;
-
- var val = reader.ReadElementContentAsString();
-
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
- {
- item.PremiereDate = date;
- item.ProductionYear = date.Year;
- }
-
- break;
+ item.PremiereDate = releaseDate;
+ item.ProductionYear = releaseDate.Year;
}
+ break;
case "enddate":
+ if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var endDate))
{
- var formatString = nfoConfiguration.ReleaseDateFormat;
-
- var val = reader.ReadElementContentAsString();
-
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
- {
- item.EndDate = date;
- }
-
- break;
+ item.EndDate = endDate;
}
+ break;
case "genre":
{
var val = reader.ReadElementContentAsString();
@@ -721,57 +533,34 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "style":
case "tag":
+ var tag = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(tag))
{
- var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.AddTag(val);
- }
-
- break;
+ item.AddTag(tag);
}
+ break;
case "fileinfo":
- {
- if (!reader.IsEmptyElement)
- {
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromFileInfoNode(subtree, item);
- }
- }
- else
- {
- reader.Read();
- }
-
- break;
- }
-
+ FetchFromFileInfoNode(reader, item);
+ break;
case "uniqueid":
+ if (reader.IsEmptyElement)
{
- if (reader.IsEmptyElement)
- {
- reader.Read();
- break;
- }
-
- var provider = reader.GetAttribute("type");
- var id = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(id))
- {
- item.SetProviderId(provider, id);
- }
-
+ reader.Read();
break;
}
- case "thumb":
+ var provider = reader.GetAttribute("type");
+ var providerId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(providerId))
{
- FetchThumbNode(reader, itemResult, "thumb");
- break;
+ item.SetProviderId(provider, providerId);
}
+ break;
+ case "thumb":
+ FetchThumbNode(reader, itemResult, "thumb");
+ break;
case "fanart":
{
if (reader.IsEmptyElement)
@@ -876,242 +665,188 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
- private void FetchFromFileInfoNode(XmlReader reader, T item)
+ private void FetchFromFileInfoNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType != XmlNodeType.Element)
{
- switch (reader.Name)
- {
- case "streamdetails":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromStreamDetailsNode(subtree, item);
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
- else
+
+ switch (reader.Name)
{
- reader.Read();
+ case "streamdetails":
+ FetchFromStreamDetailsNode(reader, item);
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
}
- private void FetchFromStreamDetailsNode(XmlReader reader, T item)
+ private void FetchFromStreamDetailsNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType != XmlNodeType.Element)
{
- switch (reader.Name)
- {
- case "video":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromVideoNode(subtree, item);
- }
-
- break;
- }
-
- case "subtitle":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromSubtitleNode(subtree, item);
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
- else
+
+ switch (reader.Name)
{
- reader.Read();
+ case "video":
+ FetchFromVideoNode(reader, item);
+ break;
+ case "subtitle":
+ FetchFromSubtitleNode(reader, item);
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
}
- private void FetchFromVideoNode(XmlReader reader, T item)
+ private void FetchFromVideoNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType != XmlNodeType.Element || item is not Video video)
{
- switch (reader.Name)
- {
- case "format3d":
- {
- var val = reader.ReadElementContentAsString();
-
- var video = item as Video;
-
- if (video is not null)
- {
- if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.HalfSideBySide;
- }
- else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
- }
- else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.FullTopAndBottom;
- }
- else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.FullSideBySide;
- }
- else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.MVC;
- }
- }
-
- break;
- }
-
- case "aspect":
- {
- var val = reader.ReadElementContentAsString();
-
- if (item is Video video)
- {
- video.AspectRatio = val;
- }
-
- break;
- }
-
- case "width":
- {
- var val = reader.ReadElementContentAsInt();
-
- if (item is Video video)
- {
- video.Width = val;
- }
-
- break;
- }
-
- case "height":
- {
- var val = reader.ReadElementContentAsInt();
-
- if (item is Video video)
- {
- video.Height = val;
- }
-
- break;
- }
-
- case "durationinseconds":
- {
- var val = reader.ReadElementContentAsInt();
-
- if (item is Video video)
- {
- video.RunTimeTicks = new TimeSpan(0, 0, val).Ticks;
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
- else
+
+ switch (reader.Name)
{
- reader.Read();
+ case "format3d":
+ var format = reader.ReadElementContentAsString();
+ if (string.Equals("HSBS", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals("HTAB", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
+ }
+ else if (string.Equals("FTAB", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullTopAndBottom;
+ }
+ else if (string.Equals("FSBS", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullSideBySide;
+ }
+ else if (string.Equals("MVC", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.MVC;
+ }
+
+ break;
+ case "aspect":
+ video.AspectRatio = reader.ReadNormalizedString();
+ break;
+ case "width":
+ video.Width = reader.ReadElementContentAsInt();
+ break;
+ case "height":
+ video.Height = reader.ReadElementContentAsInt();
+ break;
+ case "durationinseconds":
+ video.RunTimeTicks = new TimeSpan(0, 0, reader.ReadElementContentAsInt()).Ticks;
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
}
- private void FetchFromSubtitleNode(XmlReader reader, T item)
+ private void FetchFromSubtitleNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType != XmlNodeType.Element)
{
- switch (reader.Name)
- {
- case "language":
- _ = reader.ReadElementContentAsString();
- if (item is Video video)
- {
- video.HasSubtitles = true;
- }
-
- break;
-
- default:
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
- else
+
+ switch (reader.Name)
{
- reader.Read();
+ case "language":
+ _ = reader.ReadElementContentAsString();
+ if (item is Video video)
+ {
+ video.HasSubtitles = true;
+ }
+
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
}
- private void FetchFromRatingsNode(XmlReader reader, T item)
+ private void FetchFromRatingsNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
@@ -1196,102 +931,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
- /// <summary>
- /// Gets the persons from a XML node.
- /// </summary>
- /// <param name="reader">The <see cref="XmlReader"/>.</param>
- /// <returns>IEnumerable{PersonInfo}.</returns>
- private PersonInfo GetPersonFromXmlNode(XmlReader reader)
- {
- var name = string.Empty;
- var type = PersonKind.Actor; // If type is not specified assume actor
- var role = string.Empty;
- int? sortOrder = null;
- string? imageUrl = null;
-
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "name":
- name = reader.ReadElementContentAsString();
- break;
-
- case "role":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- role = val;
- }
-
- break;
- }
-
- case "type":
- {
- var val = reader.ReadElementContentAsString();
- if (!Enum.TryParse(val, true, out type))
- {
- type = PersonKind.Actor;
- }
-
- break;
- }
-
- case "order":
- case "sortorder":
- {
- var val = reader.ReadElementContentAsString();
-
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
- {
- sortOrder = intVal;
- }
-
- break;
- }
-
- case "thumb":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- imageUrl = val;
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return new PersonInfo
- {
- Name = name.Trim(),
- Role = role,
- Type = type,
- SortOrder = sortOrder,
- ImageUrl = imageUrl
- };
- }
-
internal XmlReaderSettings GetXmlReaderSettings()
=> new XmlReaderSettings()
{
@@ -1302,24 +941,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
};
/// <summary>
- /// Used to split names of comma or pipe delimited genres and people.
- /// </summary>
- /// <param name="value">The value.</param>
- /// <returns>IEnumerable{System.String}.</returns>
- private IEnumerable<string> SplitNames(string value)
- {
- // Only split by comma if there is no pipe in the string
- // We have to be careful to not split names like Matthew, Jr.
- var separator = !value.Contains('|', StringComparison.Ordinal) && !value.Contains(';', StringComparison.Ordinal)
- ? new[] { ',' }
- : new[] { '|', ';' };
-
- value = value.Trim().Trim(separator);
-
- return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : value.Split(separator, StringSplitOptions.RemoveEmptyEntries);
- }
-
- /// <summary>
/// Parses the <see cref="ImageType"/> from the NFO aspect property.
/// </summary>
/// <param name="aspect">The NFO aspect property.</param>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index d2f349ad7..044efb51e 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -1,10 +1,11 @@
using System;
-using System.Globalization;
using System.IO;
+using System.Text;
using System.Threading;
using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
// Extract the last episode number from nfo
+ // Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
+ var name = new StringBuilder(item.Item.Name);
+ var overview = new StringBuilder(item.Item.Overview);
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
{
xml = xmlFile.Substring(0, index + srch.Length);
@@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
reader.MoveToContent();
- if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num))
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "name":
+ case "title":
+ case "localtitle":
+ name.Append(" / ").Append(reader.ReadElementContentAsString());
+ break;
+ case "episode":
+ {
+ if (int.TryParse(reader.ReadElementContentAsString(), out var num))
+ {
+ item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
+ }
+
+ break;
+ }
+
+ case "biography":
+ case "plot":
+ case "review":
+ overview.Append(" / ").Append(reader.ReadElementContentAsString());
+ break;
+ }
+ }
+
+ reader.Read();
}
}
}
+
+ item.Item.Name = name.ToString();
+ item.Item.Overview = overview.ToString();
}
catch (XmlException)
{
@@ -112,142 +148,53 @@ namespace MediaBrowser.XbmcMetadata.Parsers
switch (reader.Name)
{
case "season":
+ if (reader.TryReadInt(out var seasonNumber))
{
- var number = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(number))
- {
- if (int.TryParse(number, out var num))
- {
- item.ParentIndexNumber = num;
- }
- }
-
- break;
+ item.ParentIndexNumber = seasonNumber;
}
+ break;
case "episode":
+ if (reader.TryReadInt(out var episodeNumber))
{
- var number = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(number))
- {
- if (int.TryParse(number, out var num))
- {
- item.IndexNumber = num;
- }
- }
-
- break;
+ item.IndexNumber = episodeNumber;
}
+ break;
case "episodenumberend":
+ if (reader.TryReadInt(out var episodeNumberEnd))
{
- var number = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(number))
- {
- if (int.TryParse(number, out var num))
- {
- item.IndexNumberEnd = num;
- }
- }
-
- break;
+ item.IndexNumberEnd = episodeNumberEnd;
}
+ break;
case "airsbefore_episode":
+ case "displayepisode":
+ if (reader.TryReadInt(out var airsBeforeEpisode))
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- // int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
- {
- item.AirsBeforeEpisodeNumber = rval;
- }
- }
-
- break;
+ item.AirsBeforeEpisodeNumber = airsBeforeEpisode;
}
+ break;
case "airsafter_season":
+ case "displayafterseason":
+ if (reader.TryReadInt(out var airsAfterSeason))
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- // int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
- {
- item.AirsAfterSeasonNumber = rval;
- }
- }
-
- break;
+ item.AirsAfterSeasonNumber = airsAfterSeason;
}
+ break;
case "airsbefore_season":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- // int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
- {
- item.AirsBeforeSeasonNumber = rval;
- }
- }
-
- break;
- }
-
case "displayseason":
+ if (reader.TryReadInt(out var airsBeforeSeason))
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- // int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
- {
- item.AirsBeforeSeasonNumber = rval;
- }
- }
-
- break;
- }
-
- case "displayepisode":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- // int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
- {
- item.AirsBeforeEpisodeNumber = rval;
- }
- }
-
- break;
+ item.AirsBeforeSeasonNumber = airsBeforeSeason;
}
+ break;
case "showtitle":
- {
- var showtitle = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(showtitle))
- {
- item.SeriesName = showtitle;
- }
-
- break;
- }
-
+ item.SeriesName = reader.ReadNormalizedString();
+ break;
default:
base.FetchDataFromXmlNode(reader, itemResult);
break;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index ecfed6873..16ea5e3ea 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -5,6 +5,7 @@ using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -113,31 +114,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "artist":
+ var artist = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo)
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
- {
- var list = movie.Artists.ToList();
- list.Add(val);
- movie.Artists = list.ToArray();
- }
-
- break;
+ var list = artistVideo.Artists.ToList();
+ list.Add(artist);
+ artistVideo.Artists = list.ToArray();
}
+ break;
case "album":
+ var album = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(album) && item is MusicVideo albumVideo)
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
- {
- movie.Album = val;
- }
-
- break;
+ albumVideo.Album = album;
}
+ break;
default:
base.FetchDataFromXmlNode(reader, itemResult);
break;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
index 51d5f932b..e13f0d997 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
@@ -1,7 +1,7 @@
-using System.Globalization;
using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -41,32 +41,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
switch (reader.Name)
{
case "seasonnumber":
+ if (reader.TryReadInt(out var seasonNumber))
{
- var number = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(number))
- {
- if (int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
- {
- item.IndexNumber = num;
- }
- }
-
- break;
+ item.IndexNumber = seasonNumber;
}
+ break;
case "seasonname":
- {
- var name = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(name))
- {
- item.Name = name;
- }
-
- break;
- }
-
+ item.Name = reader.ReadNormalizedString();
+ break;
default:
base.FetchDataFromXmlNode(reader, itemResult);
break;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index f22b861eb..dbcfe7997 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -1,9 +1,9 @@
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -76,23 +76,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "airs_dayofweek":
- {
- item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
- break;
- }
-
+ item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
+ break;
case "airs_time":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.AirTime = val;
- }
-
- break;
- }
-
+ item.AirTime = reader.ReadNormalizedString();
+ break;
case "status":
{
var status = reader.ReadElementContentAsString();
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
index af581fc5d..9b4e1731d 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
@@ -13,7 +13,7 @@ namespace MediaBrowser.XbmcMetadata.Providers
public abstract class BaseNfoProvider<T> : ILocalMetadataProvider<T>, IHasItemChangeMonitor
where T : BaseItem, new()
{
- private IFileSystem _fileSystem;
+ private readonly IFileSystem _fileSystem;
protected BaseNfoProvider(IFileSystem fileSystem)
{
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index 82e1dc860..8fa22fad9 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
else
{
- yield return Path.ChangeExtension(item.Path, ".nfo");
-
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
{
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
}
+
+ yield return Path.ChangeExtension(item.Path, ".nfo");
}
}
diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs
index a1b4627a9..fab70eae2 100644
--- a/RSSDP/HttpRequestParser.cs
+++ b/RSSDP/HttpRequestParser.cs
@@ -33,10 +33,7 @@ namespace Rssdp.Infrastructure
}
finally
{
- if (retVal != null)
- {
- retVal.Dispose();
- }
+ retVal?.Dispose();
}
}
diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs
index 71b7a7b99..c570c84cb 100644
--- a/RSSDP/HttpResponseParser.cs
+++ b/RSSDP/HttpResponseParser.cs
@@ -33,10 +33,7 @@ namespace Rssdp.Infrastructure
}
catch
{
- if (retVal != null)
- {
- retVal.Dispose();
- }
+ retVal?.Dispose();
throw;
}
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index 0dce6c3bf..42563e2ed 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -32,10 +32,10 @@ namespace Rssdp.Infrastructure
* port to use, we will default to 0 which allows the underlying system to auto-assign a free port.
*/
- private object _BroadcastListenSocketSynchroniser = new object();
+ private object _BroadcastListenSocketSynchroniser = new();
private List<Socket> _MulticastListenSockets;
- private object _SendSocketSynchroniser = new object();
+ private object _SendSocketSynchroniser = new();
private List<Socket> _sendSockets;
private HttpRequestParser _RequestParser;
@@ -48,7 +48,6 @@ namespace Rssdp.Infrastructure
private int _MulticastTtl;
private bool _IsShared;
- private readonly bool _enableMultiSocketBinding;
/// <summary>
/// Raised when a HTTPU request message is received by a socket (unicast or multicast).
@@ -64,9 +63,11 @@ namespace Rssdp.Infrastructure
/// Minimum constructor.
/// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
- public SsdpCommunicationsServer(ISocketFactory socketFactory,
- INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
- : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
+ public SsdpCommunicationsServer(
+ ISocketFactory socketFactory,
+ INetworkManager networkManager,
+ ILogger logger)
+ : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger)
{
}
@@ -76,7 +77,12 @@ namespace Rssdp.Infrastructure
/// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception>
- public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
+ public SsdpCommunicationsServer(
+ ISocketFactory socketFactory,
+ int localPort,
+ int multicastTimeToLive,
+ INetworkManager networkManager,
+ ILogger logger)
{
if (socketFactory is null)
{
@@ -88,19 +94,18 @@ namespace Rssdp.Infrastructure
throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero.");
}
- _BroadcastListenSocketSynchroniser = new object();
- _SendSocketSynchroniser = new object();
+ _BroadcastListenSocketSynchroniser = new();
+ _SendSocketSynchroniser = new();
_LocalPort = localPort;
_SocketFactory = socketFactory;
- _RequestParser = new HttpRequestParser();
- _ResponseParser = new HttpResponseParser();
+ _RequestParser = new();
+ _ResponseParser = new();
_MulticastTtl = multicastTimeToLive;
_networkManager = networkManager;
_logger = logger;
- _enableMultiSocketBinding = enableMultiSocketBinding;
}
/// <summary>
@@ -335,7 +340,7 @@ namespace Rssdp.Infrastructure
{
sockets = sockets.ToList();
- var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address)))
+ var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))
.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
return Task.WhenAll(tasks);
}
@@ -347,33 +352,26 @@ namespace Rssdp.Infrastructure
{
var sockets = new List<Socket>();
var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress);
- if (_enableMultiSocketBinding)
- {
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses()
- .Where(x => x.Address is not null)
- .Where(x => x.AddressFamily == AddressFamily.InterNetwork)
- .DistinctBy(x => x.Index);
- foreach (var intf in validInterfaces)
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses()
+ .Where(x => x.Address is not null)
+ .Where(x => x.SupportsMulticast)
+ .Where(x => x.AddressFamily == AddressFamily.InterNetwork)
+ .DistinctBy(x => x.Index);
+
+ foreach (var intf in validInterfaces)
+ {
+ try
{
- try
- {
- var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address);
- }
+ var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
+ _ = ListenToSocketInternal(socket);
+ sockets.Add(socket);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
}
- }
- else
- {
- var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
}
return sockets;
@@ -382,34 +380,32 @@ namespace Rssdp.Infrastructure
private List<Socket> CreateSendSockets()
{
var sockets = new List<Socket>();
- if (_enableMultiSocketBinding)
+
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses()
+ .Where(x => x.Address is not null)
+ .Where(x => x.SupportsMulticast)
+ .Where(x => x.AddressFamily == AddressFamily.InterNetwork);
+
+ if (OperatingSystem.IsMacOS())
{
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses()
- .Where(x => x.Address is not null)
- .Where(x => x.AddressFamily == AddressFamily.InterNetwork);
+ // Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340
+ validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback));
+ }
- foreach (var intf in validInterfaces)
+ foreach (var intf in validInterfaces)
+ {
+ try
{
- try
- {
- var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address);
- }
+ var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
+ _ = ListenToSocketInternal(socket);
+ sockets.Add(socket);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
}
}
- else
- {
- var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
- }
-
return sockets;
}
@@ -423,7 +419,7 @@ namespace Rssdp.Infrastructure
{
try
{
- var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);;
+ var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);
if (result.ReceivedBytes > 0)
{
@@ -431,7 +427,7 @@ namespace Rssdp.Infrastructure
var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface);
ProcessMessage(
- UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
+ Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
remoteEndpoint,
localEndpointAdapter.Address);
}
@@ -511,23 +507,17 @@ namespace Rssdp.Infrastructure
return;
}
- var handlers = this.RequestReceived;
- if (handlers is not null)
- {
- handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress));
- }
+ var handlers = RequestReceived;
+ handlers?.Invoke(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress));
}
private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress)
{
- var handlers = this.ResponseReceived;
- if (handlers is not null)
+ var handlers = ResponseReceived;
+ handlers?.Invoke(this, new ResponseReceivedEventArgs(data, endPoint)
{
- handlers(this, new ResponseReceivedEventArgs(data, endPoint)
- {
- LocalIPAddress = localIPAddress
- });
- }
+ LocalIPAddress = localIPAddress
+ });
}
}
}
diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs
index 3e4261b6a..569d733ea 100644
--- a/RSSDP/SsdpDevice.cs
+++ b/RSSDP/SsdpDevice.cs
@@ -337,10 +337,7 @@ namespace Rssdp
protected virtual void OnDeviceAdded(SsdpEmbeddedDevice device)
{
var handlers = this.DeviceAdded;
- if (handlers != null)
- {
- handlers(this, new DeviceEventArgs(device));
- }
+ handlers?.Invoke(this, new DeviceEventArgs(device));
}
/// <summary>
@@ -352,10 +349,7 @@ namespace Rssdp
protected virtual void OnDeviceRemoved(SsdpEmbeddedDevice device)
{
var handlers = this.DeviceRemoved;
- if (handlers != null)
- {
- handlers(this, new DeviceEventArgs(device));
- }
+ handlers?.Invoke(this, new DeviceEventArgs(device));
}
}
}
diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs
index 59f4c5070..d6fad4b9d 100644
--- a/RSSDP/SsdpDeviceLocator.cs
+++ b/RSSDP/SsdpDeviceLocator.cs
@@ -17,7 +17,7 @@ namespace Rssdp.Infrastructure
private ISsdpCommunicationsServer _CommunicationsServer;
private Timer _BroadcastTimer;
- private object _timerLock = new object();
+ private object _timerLock = new();
private string _OSName;
@@ -221,19 +221,16 @@ namespace Rssdp.Infrastructure
/// <seealso cref="DeviceAvailable"/>
protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return;
}
- var handlers = this.DeviceAvailable;
- if (handlers is not null)
+ var handlers = DeviceAvailable;
+ handlers?.Invoke(this, new DeviceAvailableEventArgs(device, isNewDevice)
{
- handlers(this, new DeviceAvailableEventArgs(device, isNewDevice)
- {
- RemoteIPAddress = IPAddress
- });
- }
+ RemoteIPAddress = IPAddress
+ });
}
/// <summary>
@@ -244,16 +241,13 @@ namespace Rssdp.Infrastructure
/// <seealso cref="DeviceUnavailable"/>
protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired)
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return;
}
- var handlers = this.DeviceUnavailable;
- if (handlers is not null)
- {
- handlers(this, new DeviceUnavailableEventArgs(device, expired));
- }
+ var handlers = DeviceUnavailable;
+ handlers?.Invoke(this, new DeviceUnavailableEventArgs(device, expired));
}
/// <summary>
@@ -291,8 +285,8 @@ namespace Rssdp.Infrastructure
_CommunicationsServer = null;
if (commsServer is not null)
{
- commsServer.ResponseReceived -= this.CommsServer_ResponseReceived;
- commsServer.RequestReceived -= this.CommsServer_RequestReceived;
+ commsServer.ResponseReceived -= CommsServer_ResponseReceived;
+ commsServer.RequestReceived -= CommsServer_RequestReceived;
}
}
}
@@ -341,7 +335,7 @@ namespace Rssdp.Infrastructure
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- values["HOST"] = "239.255.255.250:1900";
+ values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort);
values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
values["MAN"] = "\"ssdp:discover\"";
@@ -382,17 +376,17 @@ namespace Rssdp.Infrastructure
private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress)
{
- if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
+ if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
{
return;
}
var notificationType = GetFirstHeaderStringValue("NTS", message);
- if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
+ if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
{
ProcessAliveNotification(message, IPAddress);
}
- else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
+ else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
{
ProcessByeByeNotification(message);
}
@@ -420,7 +414,7 @@ namespace Rssdp.Infrastructure
private void ProcessByeByeNotification(HttpRequestMessage message)
{
var notficationType = GetFirstHeaderStringValue("NT", message);
- if (!String.IsNullOrEmpty(notficationType))
+ if (!string.IsNullOrEmpty(notficationType))
{
var usn = GetFirstHeaderStringValue("USN", message);
@@ -447,10 +441,9 @@ namespace Rssdp.Infrastructure
private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message)
{
string retVal = null;
- IEnumerable<string> values;
if (message.Headers.Contains(headerName))
{
- message.Headers.TryGetValues(headerName, out values);
+ message.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
retVal = values.FirstOrDefault();
@@ -463,10 +456,9 @@ namespace Rssdp.Infrastructure
private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message)
{
string retVal = null;
- IEnumerable<string> values;
if (message.Headers.Contains(headerName))
{
- message.Headers.TryGetValues(headerName, out values);
+ message.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
retVal = values.FirstOrDefault();
@@ -479,10 +471,9 @@ namespace Rssdp.Infrastructure
private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request)
{
string value = null;
- IEnumerable<string> values;
if (request.Headers.Contains(headerName))
{
- request.Headers.TryGetValues(headerName, out values);
+ request.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
value = values.FirstOrDefault();
@@ -496,10 +487,9 @@ namespace Rssdp.Infrastructure
private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response)
{
string value = null;
- IEnumerable<string> values;
if (response.Headers.Contains(headerName))
{
- response.Headers.TryGetValues(headerName, out values);
+ response.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
value = values.FirstOrDefault();
@@ -529,7 +519,7 @@ namespace Rssdp.Infrastructure
foreach (var device in expiredDevices)
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return;
}
@@ -543,7 +533,7 @@ namespace Rssdp.Infrastructure
// problems.
foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct())
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return;
}
@@ -560,7 +550,7 @@ namespace Rssdp.Infrastructure
existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn);
foreach (var existingDevice in existingDevices)
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return true;
}
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index 950e6fec8..0ac9cc9a1 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -206,9 +206,9 @@ namespace Rssdp.Infrastructure
IPAddress receivedOnlocalIPAddress,
CancellationToken cancellationToken)
{
- if (String.IsNullOrEmpty(searchTarget))
+ if (string.IsNullOrEmpty(searchTarget))
{
- WriteTrace(String.Format(CultureInfo.InvariantCulture, "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;
}
@@ -232,7 +232,7 @@ namespace Rssdp.Infrastructure
// return;
}
- if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0)
+ if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0)
{
return;
}
@@ -243,27 +243,27 @@ namespace Rssdp.Infrastructure
}
// Do not block synchronously as that may tie up a threadpool thread for several seconds.
- Task.Delay(_Random.Next(16, (maxWaitInterval * 1000))).ContinueWith((parentTask) =>
+ Task.Delay(_Random.Next(16, maxWaitInterval * 1000), cancellationToken).ContinueWith((parentTask) =>
{
// Copying devices to local array here to avoid threading issues/enumerator exceptions.
IEnumerable<SsdpDevice> devices = null;
lock (_Devices)
{
- if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)
+ if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)
{
devices = GetAllDevicesAsFlatEnumerable().ToArray();
}
- else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0))
+ else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0))
{
devices = _Devices.ToArray();
}
else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase))
{
- devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray();
+ devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray();
}
else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase))
{
- devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
+ devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
}
}
@@ -281,7 +281,7 @@ namespace Rssdp.Infrastructure
}
}
}
- });
+ }, cancellationToken);
}
private IEnumerable<SsdpDevice> GetAllDevicesAsFlatEnumerable()
@@ -299,7 +299,7 @@ namespace Rssdp.Infrastructure
if (isRootDevice)
{
SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
- if (this.SupportPnpRootDevice)
+ if (SupportPnpRootDevice)
{
SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
}
@@ -312,7 +312,7 @@ namespace Rssdp.Infrastructure
private string GetUsn(string udn, string fullDeviceType)
{
- return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
+ return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
}
private async void SendSearchResponse(
@@ -326,16 +326,17 @@ namespace Rssdp.Infrastructure
const string header = "HTTP/1.1 200 OK";
var rootDevice = device.ToRootDevice();
- var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- values["EXT"] = "";
- values["DATE"] = DateTime.UtcNow.ToString("r");
- values["HOST"] = "239.255.255.250:1900";
- values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
- values["ST"] = searchTarget;
- values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
- values["USN"] = uniqueServiceName;
- values["LOCATION"] = rootDevice.Location.ToString();
+ var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ ["EXT"] = "",
+ ["DATE"] = DateTime.UtcNow.ToString("r"),
+ ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
+ ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
+ ["ST"] = searchTarget,
+ ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
+ ["USN"] = uniqueServiceName,
+ ["LOCATION"] = rootDevice.Location.ToString()
+ };
var message = BuildMessage(header, values);
@@ -439,7 +440,7 @@ namespace Rssdp.Infrastructure
if (isRoot)
{
SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken);
- if (this.SupportPnpRootDevice)
+ if (SupportPnpRootDevice)
{
SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken);
}
@@ -460,17 +461,18 @@ namespace Rssdp.Infrastructure
const string header = "NOTIFY * HTTP/1.1";
- var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- // If needed later for non-server devices, these headers will need to be dynamic
- values["HOST"] = "239.255.255.250:1900";
- values["DATE"] = DateTime.UtcNow.ToString("r");
- values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
- values["LOCATION"] = rootDevice.Location.ToString();
- values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
- values["NTS"] = "ssdp:alive";
- values["NT"] = notificationType;
- values["USN"] = uniqueServiceName;
+ var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ // If needed later for non-server devices, these headers will need to be dynamic
+ ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
+ ["DATE"] = DateTime.UtcNow.ToString("r"),
+ ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
+ ["LOCATION"] = rootDevice.Location.ToString(),
+ ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
+ ["NTS"] = "ssdp:alive",
+ ["NT"] = notificationType,
+ ["USN"] = uniqueServiceName
+ };
var message = BuildMessage(header, values);
@@ -485,7 +487,7 @@ namespace Rssdp.Infrastructure
if (isRoot)
{
tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken));
- if (this.SupportPnpRootDevice)
+ if (SupportPnpRootDevice)
{
tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken));
}
@@ -506,20 +508,21 @@ namespace Rssdp.Infrastructure
{
const string header = "NOTIFY * HTTP/1.1";
- var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- // If needed later for non-server devices, these headers will need to be dynamic
- values["HOST"] = "239.255.255.250:1900";
- values["DATE"] = DateTime.UtcNow.ToString("r");
- values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
- values["NTS"] = "ssdp:byebye";
- values["NT"] = notificationType;
- values["USN"] = uniqueServiceName;
+ var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ // If needed later for non-server devices, these headers will need to be dynamic
+ ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
+ ["DATE"] = DateTime.UtcNow.ToString("r"),
+ ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
+ ["NTS"] = "ssdp:byebye",
+ ["NT"] = notificationType,
+ ["USN"] = uniqueServiceName
+ };
var message = BuildMessage(header, values);
var sendCount = IsDisposed ? 1 : 3;
- WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
+ WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
}
@@ -527,10 +530,7 @@ namespace Rssdp.Infrastructure
{
var timer = _RebroadcastAliveNotificationsTimer;
_RebroadcastAliveNotificationsTimer = null;
- if (timer is not null)
- {
- timer.Dispose();
- }
+ timer?.Dispose();
}
private TimeSpan GetMinimumNonZeroCacheLifetime()
@@ -564,10 +564,7 @@ namespace Rssdp.Infrastructure
private void WriteTrace(string text)
{
- if (LogFunction is not null)
- {
- LogFunction(text);
- }
+ LogFunction?.Invoke(text);
// System.Diagnostics.Debug.WriteLine(text, "SSDP Publisher");
}
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
index 912996755..af460fedc 100644
--- a/debian/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -24,6 +24,9 @@ JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web"
# ffmpeg binary paths, overriding the system values
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
+# Disable glibc dynamic heap adjustment
+MALLOC_TRIM_THRESHOLD_=131072
+
# [OPTIONAL] run Jellyfin as a headless service
#JELLYFIN_SERVICE_OPT="--service"
@@ -47,4 +50,4 @@ JELLYFIN_ADDITIONAL_OPTS=""
# Application username
JELLYFIN_USER="jellyfin"
# Full application command
-JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS"
+JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS --datadir $JELLYFIN_DATA_DIR --configdir $JELLYFIN_CONFIG_DIR --logdir $JELLYFIN_LOG_DIR --cachedir $JELLYFIN_CACHE_DIR"
diff --git a/debian/jellyfin.init b/debian/jellyfin.init
index 7f5642bac..784536d87 100644
--- a/debian/jellyfin.init
+++ b/debian/jellyfin.init
@@ -1,3 +1,4 @@
+#!/bin/sh
### BEGIN INIT INFO
# Provides: Jellyfin Media Server
# Required-Start: $local_fs $network
diff --git a/debian/rules b/debian/rules
index f55b1807e..069d48aad 100755
--- a/debian/rules
+++ b/debian/rules
@@ -25,6 +25,10 @@ ifeq ($(HOST_ARCH),arm64)
# Building ARM
DOTNETRUNTIME := debian-arm64
endif
+ifeq ($(HOST_ARCH),aarch64)
+ # Building ARM
+ DOTNETRUNTIME := debian-arm64
+endif
export DH_VERBOSE=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index da986a07e..e5cf638c1 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 09f93d41b..777d92c11 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 9910773da..f34c0358d 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-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 aa69b27f4..87466d20e 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-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 bb8597b41..261deb3e4 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ff8c660f-ffa9-4814-ac2d-4089e6ec4eb5/dc806d344844f1d58d8015d105e85c65/dotnet-sdk-7.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/fedora/jellyfin-selinux-launcher.sh b/fedora/jellyfin-selinux-launcher.sh
new file mode 100644
index 000000000..e07a351d9
--- /dev/null
+++ b/fedora/jellyfin-selinux-launcher.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec /usr/lib64/jellyfin/jellyfin "${@}"
diff --git a/fedora/jellyfin.env b/fedora/jellyfin.env
index 1f79fac4f..cee8f6854 100644
--- a/fedora/jellyfin.env
+++ b/fedora/jellyfin.env
@@ -23,6 +23,12 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
# web client path, installed by the jellyfin-web package
# JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web"
+# In-App service control
+JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
+
+# Disable glibc dynamic heap adjustment
+MALLOC_TRIM_THRESHOLD_=131072
+
# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service
index 1b3f8032c..01accdc0c 100644
--- a/fedora/jellyfin.service
+++ b/fedora/jellyfin.service
@@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin
User = jellyfin
Group = jellyfin
WorkingDirectory = /var/lib/jellyfin
-ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
+ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
Restart = on-failure
TimeoutSec = 15
SuccessExitStatus=0 143
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index a759b29b1..e78368906 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -14,6 +14,7 @@ License: GPLv2
URL: https://jellyfin.org
# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
Source0: jellyfin-server-%{version}.tar.gz
+Source10: jellyfin-selinux-launcher.sh
Source11: jellyfin.service
Source12: jellyfin.env
Source13: jellyfin.override.conf
@@ -73,7 +74,8 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
# Jellyfin files
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
-ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
+%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
+sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin
# Jellyfin config
%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
@@ -106,6 +108,7 @@ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
%attr(755,root,root) %{_libdir}/jellyfin/createdump
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
%{_libdir}/jellyfin/*
+%attr(755,root,root) %{_bindir}/jellyfin
# Jellyfin config
%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
index 03b296494..1571b5ab0 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
@@ -6,6 +6,7 @@ using Emby.Server.Implementations.Library;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Configuration;
using Moq;
using SharpFuzz;
@@ -54,8 +55,16 @@ namespace Emby.Server.Implementations.Fuzz
appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
.Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal));
+ var configSection = new Mock<IConfigurationSection>();
+ configSection.SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)])
+ .Returns("0");
+ var config = new Mock<IConfiguration>();
+ config.Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)))
+ .Returns(configSection.Object);
+
IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
fixture.Inject(appHost);
+ fixture.Inject(config);
return fixture.Create<SqliteItemRepository>();
}
}
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
index 37e6bdb76..aa2a34cdd 100755
--- a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
+++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
@@ -8,4 +8,4 @@ cp bin/Emby.Server.Implementations.dll .
dotnet build
mkdir -p Findings
-AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net6.0/Emby.Server.Implementations.Fuzz.dll "$1"
+AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net7.0/Emby.Server.Implementations.Fuzz "$1"
diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
index 20bc4c724..da46e63a5 100644
--- a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
+++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
@@ -6,8 +6,8 @@
</PropertyGroup>
<ItemGroup>
- <Reference Include="Jellyfin.Server">
- <HintPath>jellyfin.dll</HintPath>
+ <Reference Include="Jellyfin.Api">
+ <HintPath>Jellyfin.Api.dll</HintPath>
</Reference>
</ItemGroup>
diff --git a/fuzz/Jellyfin.Server.Fuzz/Program.cs b/fuzz/Jellyfin.Api.Fuzz/Program.cs
index e47286c13..6713322ac 100644
--- a/fuzz/Jellyfin.Server.Fuzz/Program.cs
+++ b/fuzz/Jellyfin.Api.Fuzz/Program.cs
@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
-using Jellyfin.Server.Middleware;
+using Jellyfin.Api.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using SharpFuzz;
-namespace Emby.Server.Implementations.Fuzz
+namespace Jellyfin.Api.Fuzz
{
public static class Program
{
diff --git a/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt b/fuzz/Jellyfin.Api.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
index 73f356b93..73f356b93 100644
--- a/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
+++ b/fuzz/Jellyfin.Api.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
diff --git a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh
new file mode 100755
index 000000000..edf965562
--- /dev/null
+++ b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+set -e
+
+dotnet build -c Release ../../Jellyfin.Api/Jellyfin.Api.csproj --output bin
+sharpfuzz bin/Jellyfin.Api.dll
+cp bin/Jellyfin.Api.dll .
+
+dotnet build
+mkdir -p Findings
+AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net7.0/Jellyfin.Api.Fuzz "$1"
diff --git a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh
deleted file mode 100755
index 303eb2135..000000000
--- a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-
-set -e
-
-dotnet build -c Release ../../Jellyfin.Server/Jellyfin.Server.csproj --output bin
-sharpfuzz bin/jellyfin.dll
-cp bin/jellyfin.dll .
-
-dotnet build
-mkdir -p Findings
-AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net6.0/Jellyfin.Server.Fuzz.dll "$1"
diff --git a/fuzz/README.md b/fuzz/README.md
new file mode 100644
index 000000000..25ba7d05c
--- /dev/null
+++ b/fuzz/README.md
@@ -0,0 +1,20 @@
+# Jellyfin fuzzing
+
+## Setup
+
+Install AFL++
+```sh
+git clone https://github.com/AFLplusplus/AFLplusplus
+cd AFLplusplus
+make all
+sudo make install
+```
+
+Install SharpFuzz.CommandLine global .NET tool
+```sh
+dotnet tool install --global SharpFuzz.CommandLine
+```
+
+## Running
+Run the `fuzz.sh` in the directory corresponding to the project you want to fuzz.
+The script takes a parameter of which fuzz case you want to run.
diff --git a/global.json b/global.json
new file mode 100644
index 000000000..24335d7a0
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "7.0.0",
+ "rollForward": "latestMinor"
+ }
+}
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index c846e2cd4..870cf253f 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -52,6 +52,8 @@
<Rule Id="SA1204" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
+ <!-- disable warning SA1311: Static readonly fields should begin with upper-case letter -->
+ <Rule Id="SA1311" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
@@ -89,6 +91,8 @@
<Rule Id="CA1727" Action="Error" />
<!-- error on CA1813: Avoid unsealed attributes -->
<Rule Id="CA1813" Action="Error" />
+ <!-- error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string -->
+ <Rule Id="CA1834" Action="Error" />
<!-- error on CA1843: Do not use 'WaitAll' with a single task -->
<Rule Id="CA1843" Action="Error" />
<!-- error on CA1845: Use span-based 'string.Concat' -->
@@ -169,4 +173,18 @@
<!-- error on RS0030: Do not used banned APIs -->
<Rule Id="RS0030" Action="Error" />
</Rules>
+
+ <Rules AnalyzerId="IDisposableAnalyzers" RuleNamespace="IDisposableAnalyzers.Correctness">
+ <!-- disable warning IDISP001: Dispose created -->
+ <Rule Id="IDISP001" Action="Info" />
+ <!-- TODO: Enable when false positives are fixed -->
+ <!-- disable warning IDISP003: Dispose previous before re-assigning -->
+ <Rule Id="IDISP003" Action="Info" />
+ <!-- disable warning IDISP004: Don't ignore created IDisposable -->
+ <Rule Id="IDISP004" Action="Info" />
+ <!-- disable warning IDISP007: Don't dispose injected -->
+ <Rule Id="IDISP007" Action="Info" />
+ <!-- disable warning IDISP008: Don't assign member with injected and created disposables -->
+ <Rule Id="IDISP008" Action="Info" />
+ </Rules>
</RuleSet>
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 000000000..ac2726ed5
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,21 @@
+<Project>
+ <!-- Sets defaults for all projects -->
+
+ <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
+
+ <!-- Code Analyzers -->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 034691322..3c417f8ff 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -18,11 +18,11 @@
<ItemGroup>
<PackageReference Include="BlurHashSharp" />
<PackageReference Include="BlurHashSharp.SkiaSharp" />
+ <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
- <PackageReference Include="SkiaSharp.Svg" />
<PackageReference Include="SkiaSharp.HarfBuzz" />
- <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" />
+ <PackageReference Include="Svg.Skia" />
</ItemGroup>
<ItemGroup>
@@ -31,15 +31,4 @@
<ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
- <!-- Code analysers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
</Project>
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 2d980db18..5721e2882 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -10,7 +10,7 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing;
using Microsoft.Extensions.Logging;
using SkiaSharp;
-using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
+using Svg.Skia;
namespace Jellyfin.Drawing.Skia;
@@ -23,6 +23,30 @@ public class SkiaEncoder : IImageEncoder
private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths;
+ private static readonly SKImageFilter _imageFilter;
+
+#pragma warning disable CA1810
+ static SkiaEncoder()
+#pragma warning restore CA1810
+ {
+ var kernel = new[]
+ {
+ 0, -.1f, 0,
+ -.1f, 1.4f, -.1f,
+ 0, -.1f, 0,
+ };
+
+ var kernelSize = new SKSizeI(3, 3);
+ var kernelOffset = new SKPointI(1, 1);
+ _imageFilter = SKImageFilter.CreateMatrixConvolution(
+ kernelSize,
+ kernel,
+ 1f,
+ 0f,
+ kernelOffset,
+ SKShaderTileMode.Clamp,
+ true);
+ }
/// <summary>
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
@@ -119,11 +143,17 @@ public class SkiaEncoder : IImageEncoder
var extension = Path.GetExtension(path.AsSpan());
if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
{
- var svg = new SKSvg();
+ using var svg = new SKSvg();
try
{
- svg.Load(path);
- return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
+ using var picture = svg.Load(path);
+ if (picture is null)
+ {
+ _logger.LogError("Unable to determine image dimensions for {FilePath}", path);
+ return default;
+ }
+
+ return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Height));
}
catch (FormatException skiaColorException)
{
@@ -188,7 +218,7 @@ public class SkiaEncoder : IImageEncoder
return path;
}
- var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
+ var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan())));
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
Directory.CreateDirectory(directory);
File.Copy(path, tempPath, true);
@@ -200,20 +230,10 @@ public class SkiaEncoder : IImageEncoder
{
if (!orientation.HasValue)
{
- return SKEncodedOrigin.TopLeft;
+ return SKEncodedOrigin.Default;
}
- return orientation.Value switch
- {
- ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
- ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
- ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
- ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
- ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
- ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
- ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
- _ => SKEncodedOrigin.TopLeft
- };
+ return (SKEncodedOrigin)orientation.Value;
}
/// <summary>
@@ -295,10 +315,7 @@ public class SkiaEncoder : IImageEncoder
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{
- var needsFlip = origin == SKEncodedOrigin.LeftBottom
- || origin == SKEncodedOrigin.LeftTop
- || origin == SKEncodedOrigin.RightBottom
- || origin == SKEncodedOrigin.RightTop;
+ var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop;
var rotated = needsFlip
? new SKBitmap(bitmap.Height, bitmap.Width)
: new SKBitmap(bitmap.Width, bitmap.Height);
@@ -363,25 +380,7 @@ public class SkiaEncoder : IImageEncoder
IsDither = isDither
};
- var kernel = new float[9]
- {
- 0, -.1f, 0,
- -.1f, 1.4f, -.1f,
- 0, -.1f, 0,
- };
-
- var kernelSize = new SKSizeI(3, 3);
- var kernelOffset = new SKPointI(1, 1);
-
- paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
- kernelSize,
- kernel,
- 1f,
- 0f,
- kernelOffset,
- SKShaderTileMode.Clamp,
- true);
-
+ paint.ImageFilter = _imageFilter;
canvas.DrawBitmap(
source,
SKRect.Create(0, 0, source.Width, source.Height),
@@ -432,7 +431,8 @@ public class SkiaEncoder : IImageEncoder
// scale image (the FromImage creates a copy)
var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
- using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
+ using var resizedImage = ResizeImage(bitmap, imageInfo);
+ using var resizedBitmap = SKBitmap.FromImage(resizedImage);
// If all we're doing is resizing then we can stop now
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
@@ -489,10 +489,8 @@ public class SkiaEncoder : IImageEncoder
Directory.CreateDirectory(directory);
using (var outputStream = new SKFileWStream(outputPath))
{
- using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
- {
- pixmap.Encode(outputStream, skiaOutputFormat, quality);
- }
+ using var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels());
+ pixmap.Encode(outputStream, skiaOutputFormat, quality);
}
return outputPath;
@@ -526,6 +524,81 @@ public class SkiaEncoder : IImageEncoder
splashBuilder.GenerateSplash(posters, backdrops, outputPath);
}
+ /// <inheritdoc />
+ public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+ {
+ var paths = options.InputPaths;
+ var tileWidth = options.Width;
+ var tileHeight = options.Height;
+
+ if (paths.Count < 1)
+ {
+ throw new ArgumentException("InputPaths cannot be empty.");
+ }
+ else if (paths.Count > tileWidth * tileHeight)
+ {
+ throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} grid.");
+ }
+
+ // If no height provided, use height of first image.
+ if (!imgHeight.HasValue)
+ {
+ using var firstImg = Decode(paths[0], false, null, out _);
+
+ if (firstImg is null)
+ {
+ throw new InvalidDataException("Could not decode image data.");
+ }
+
+ if (firstImg.Width != imgWidth)
+ {
+ throw new InvalidOperationException("Image width does not match provided width.");
+ }
+
+ imgHeight = firstImg.Height;
+ }
+
+ // Make horizontal strips using every provided image.
+ using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
+ using var canvas = new SKCanvas(tileGrid);
+
+ var imgIndex = 0;
+ for (var y = 0; y < tileHeight; y++)
+ {
+ for (var x = 0; x < tileWidth; x++)
+ {
+ if (imgIndex >= paths.Count)
+ {
+ break;
+ }
+
+ using var img = Decode(paths[imgIndex++], false, null, out _);
+
+ if (img is null)
+ {
+ throw new InvalidDataException("Could not decode image data.");
+ }
+
+ if (img.Width != imgWidth)
+ {
+ throw new InvalidOperationException("Image width does not match provided width.");
+ }
+
+ if (img.Height != imgHeight)
+ {
+ throw new InvalidOperationException("Image height does not match first image height.");
+ }
+
+ canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
+ }
+ }
+
+ using var outputStream = new SKFileWStream(options.OutputPath);
+ tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
+
+ return imgHeight.Value;
+ }
+
private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
{
try
diff --git a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
index 00d224da9..bd1b2b0da 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
@@ -19,7 +19,6 @@ public static class SkiaHelper
public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)
{
var imagesTested = new Dictionary<int, int>();
- SKBitmap? bitmap = null;
while (imagesTested.Count < paths.Count)
{
@@ -28,7 +27,7 @@ public static class SkiaHelper
currentIndex = 0;
}
- bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
+ SKBitmap? bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
imagesTested[currentIndex] = 0;
@@ -36,11 +35,12 @@ public static class SkiaHelper
if (bitmap is not null)
{
- break;
+ newIndex = currentIndex;
+ return bitmap;
}
}
newIndex = currentIndex;
- return bitmap;
+ return null;
}
}
diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index a8f80f7e2..4aff26c16 100644
--- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -38,25 +38,25 @@ public partial class StripCollageBuilder
{
ArgumentNullException.ThrowIfNull(outputPath);
- var ext = Path.GetExtension(outputPath);
+ var ext = Path.GetExtension(outputPath.AsSpan());
- if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
- || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
+ || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Jpeg;
}
- if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Webp;
}
- if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Gif;
}
- if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Bmp;
}
@@ -189,12 +189,12 @@ public partial class StripCollageBuilder
// Scale image. The FromBitmap creates a copy
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
- using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
+ using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo);
// draw this image into the strip at the next position
var xPos = x * cellWidth;
var yPos = y * cellHeight;
- canvas.DrawBitmap(resizedBitmap, xPos, yPos);
+ canvas.DrawImage(resizeImage, xPos, yPos);
}
}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 4e5d3b4d5..65a8f4e83 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -13,7 +13,6 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -109,24 +108,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
/// <inheritdoc />
- public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
- {
- var file = await ProcessImage(options).ConfigureAwait(false);
- using (var fileStream = AsyncFile.OpenRead(file.Path))
- {
- await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
- }
- }
-
- /// <inheritdoc />
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
=> _imageEncoder.SupportedOutputFormats;
/// <inheritdoc />
- public bool SupportsTransparency(string path)
- => _transparentImageTypes.Contains(Path.GetExtension(path));
-
- /// <inheritdoc />
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
{
ItemImageInfo originalImage = options.Image;
@@ -227,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
}
}
- return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
+ return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
}
catch (Exception ex)
{
@@ -265,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return ImageFormat.Jpg;
}
- private string GetMimeType(ImageFormat format, string path)
- => format switch
- {
- ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
- ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
- ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
- ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
- ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
- _ => MimeTypes.GetMimeType(path)
- };
-
/// <summary>
/// Gets the cache file path based on a set of parameters.
/// </summary>
@@ -377,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
filename.Append(",v=");
filename.Append(Version);
- return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
+ return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
}
/// <inheritdoc />
@@ -437,8 +411,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
=> (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
/// <inheritdoc />
- public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
+ public string? GetImageCacheTag(BaseItem item, ChapterInfo chapter)
{
+ if (chapter.ImagePath is null)
+ {
+ return null;
+ }
+
return GetImageCacheTag(item, new ItemImageInfo
{
Path = chapter.ImagePath,
@@ -469,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return Task.FromResult((originalImagePath, dateModified));
}
- // TODO _mediaEncoder.ConvertImage is not implemented
- // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
- // {
- // try
- // {
- // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
- //
- // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
- // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
- //
- // var file = _fileSystem.GetFileInfo(outputPath);
- // if (!file.Exists)
- // {
- // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
- // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
- // }
- // else
- // {
- // dateModified = file.LastWriteTimeUtc;
- // }
- //
- // originalImagePath = outputPath;
- // }
- // catch (Exception ex)
- // {
- // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
- // }
- // }
-
return Task.FromResult((originalImagePath, dateModified));
}
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index e0963ac34..d7ef6f8e7 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -21,15 +21,4 @@
<Compile Include="..\..\SharedVersion.cs" />
</ItemGroup>
- <!-- Code analysers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
</Project>
diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs
index 171128bed..1495661c1 100644
--- a/src/Jellyfin.Drawing/NullImageEncoder.cs
+++ b/src/Jellyfin.Drawing/NullImageEncoder.cs
@@ -50,6 +50,12 @@ public class NullImageEncoder : IImageEncoder
}
/// <inheritdoc />
+ public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
public string GetImageBlurHash(int xComp, int yComp, string path)
{
throw new NotImplementedException();
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 4f80aa941..997df6dbe 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -27,20 +27,8 @@
<Compile Include="../../SharedVersion.cs" />
</ItemGroup>
-
<ItemGroup>
<PackageReference Include="Diacritics" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
</Project>
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index 321cfa502..17096c017 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -59,7 +59,7 @@ namespace Jellyfin.Extensions.Json.Converters
var typedValueIndex = 0;
for (var i = 0; i < stringEntries.Length; i++)
{
- if (parsedValues[i] != null)
+ if (parsedValues[i] is not null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs
index 9751d9d42..182996852 100644
--- a/src/Jellyfin.Extensions/StreamExtensions.cs
+++ b/src/Jellyfin.Extensions/StreamExtensions.cs
@@ -26,10 +26,8 @@ namespace Jellyfin.Extensions
/// <returns>All lines in the stream.</returns>
public static string[] ReadAllLines(this Stream stream, Encoding encoding)
{
- using (StreamReader reader = new StreamReader(stream, encoding))
- {
- return ReadAllLines(reader).ToArray();
- }
+ using StreamReader reader = new StreamReader(stream, encoding);
+ return ReadAllLines(reader).ToArray();
}
/// <summary>
@@ -40,7 +38,7 @@ namespace Jellyfin.Extensions
public static IEnumerable<string> ReadAllLines(this TextReader reader)
{
string? line;
- while ((line = reader.ReadLine()) != null)
+ while ((line = reader.ReadLine()) is not null)
{
yield return line;
}
@@ -54,7 +52,7 @@ namespace Jellyfin.Extensions
public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader)
{
string? line;
- while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
+ while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null)
{
yield return line;
}
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index 3f4f55ee4..76dde1cf6 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -5,17 +5,6 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
index febe9516a..479e6ffdc 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
@@ -68,51 +68,54 @@ public static class FfProbeKeyframeExtractor
double streamDuration = 0;
double formatDuration = 0;
- while (!reader.EndOfStream)
+ using (reader)
{
- var line = reader.ReadLine().AsSpan();
- if (line.IsEmpty)
+ while (!reader.EndOfStream)
{
- continue;
- }
+ var line = reader.ReadLine().AsSpan();
+ if (line.IsEmpty)
+ {
+ continue;
+ }
- var firstComma = line.IndexOf(',');
- var lineType = line[..firstComma];
- var rest = line[(firstComma + 1)..];
- if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase))
- {
- // Split time and flags from the packet line. Example line: packet,7169.079000,K_
- var secondComma = rest.IndexOf(',');
- var ptsTime = rest[..secondComma];
- var flags = rest[(secondComma + 1)..];
- if (flags.StartsWith("K_"))
+ var firstComma = line.IndexOf(',');
+ var lineType = line[..firstComma];
+ var rest = line[(firstComma + 1)..];
+ if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase))
{
- if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe))
+ // Split time and flags from the packet line. Example line: packet,7169.079000,K_
+ var secondComma = rest.IndexOf(',');
+ var ptsTime = rest[..secondComma];
+ var flags = rest[(secondComma + 1)..];
+ if (flags.StartsWith("K_"))
{
- // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double.
- keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond));
+ if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe))
+ {
+ // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double.
+ keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond));
+ }
}
}
- }
- else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase))
- {
- if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult))
+ else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase))
{
- streamDuration = streamDurationResult;
+ if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult))
+ {
+ streamDuration = streamDurationResult;
+ }
}
- }
- else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase))
- {
- if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult))
+ else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase))
{
- formatDuration = formatDurationResult;
+ if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult))
+ {
+ formatDuration = formatDurationResult;
+ }
}
}
- }
- // Prefer the stream duration as it should be more accurate
- var duration = streamDuration > 0 ? streamDuration : formatDuration;
+ // Prefer the stream duration as it should be more accurate
+ var duration = streamDuration > 0 ? streamDuration : formatDuration;
- return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes);
+ return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes);
+ }
}
}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 71572bcf6..0d91a447b 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -9,17 +9,6 @@
<PackageReference Include="NEbml" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs
index 797fc8f64..1ff7e7b7a 100644
--- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
+++ b/tests/Jellyfin.Api.Tests/Middleware/UrlDecodeQueryFeatureTests.cs
@@ -1,12 +1,11 @@
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Api.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using Xunit;
-namespace Jellyfin.Server.Tests
+namespace Jellyfin.Api.Middleware.Tests
{
public static class UrlDecodeQueryFeatureTests
{
@@ -22,7 +21,7 @@ namespace Jellyfin.Server.Tests
Assert.Single(test.Query);
var (k, v) = test.Query.First();
Assert.Equal(key, k);
- Assert.Empty(v);
+ Assert.True(StringValues.IsNullOrEmpty(v));
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs
new file mode 100644
index 000000000..263f74c90
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.MediaEncoding.Encoder;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Tests.Probing
+{
+ public class ProbeExternalSourcesTests
+ {
+ [Fact]
+ public void GetExtraArguments_Forwards_UserAgent()
+ {
+ var encoder = new MediaEncoder(
+ Mock.Of<ILogger<MediaEncoder>>(),
+ Mock.Of<IServerConfigurationManager>(),
+ Mock.Of<IFileSystem>(),
+ Mock.Of<IBlurayExaminer>(),
+ Mock.Of<ILocalizationManager>(),
+ new ConfigurationBuilder().Build(),
+ Mock.Of<IServerConfigurationManager>());
+
+ var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";
+ var req = new MediaBrowser.Controller.MediaEncoding.MediaInfoRequest()
+ {
+ MediaSource = new MediaBrowser.Model.Dto.MediaSourceInfo
+ {
+ Path = "/path/to/stream",
+ Protocol = MediaProtocol.Http,
+ RequiredHttpHeaders = new Dictionary<string, string>()
+ {
+ { "user_agent", userAgent },
+ }
+ },
+ ExtractChapters = false,
+ MediaType = MediaBrowser.Model.Dlna.DlnaProfileType.Video,
+ };
+
+ var extraArg = encoder.GetExtraArguments(req);
+
+ Assert.Contains(userAgent, extraArg, StringComparison.InvariantCulture);
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
index fe0d7fc90..1f908d7e0 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
@@ -12,17 +12,16 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
[Fact]
public void Parse_Valid_Success()
{
- using (var stream = File.OpenRead("Test Data/example.ass"))
- {
- var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "ass");
- Assert.Single(parsed.TrackEvents);
- var trackEvent = parsed.TrackEvents[0];
+ using var stream = File.OpenRead("Test Data/example.ass");
- Assert.Equal("1", trackEvent.Id);
- Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
- Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text);
- }
+ var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "ass");
+ Assert.Single(parsed.TrackEvents);
+ var trackEvent = parsed.TrackEvents[0];
+
+ Assert.Equal("1", trackEvent.Id);
+ Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
+ Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
index 2aebee556..b7152961c 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -12,45 +12,43 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
[Fact]
public void Parse_Valid_Success()
{
- using (var stream = File.OpenRead("Test Data/example.srt"))
- {
- var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt");
- Assert.Equal(2, parsed.TrackEvents.Count);
-
- var trackEvent1 = parsed.TrackEvents[0];
- Assert.Equal("1", trackEvent1.Id);
- Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
- Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text);
-
- var trackEvent2 = parsed.TrackEvents[1];
- Assert.Equal("2", trackEvent2.Id);
- Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
- Assert.Equal("Very good, Lieutenant.", trackEvent2.Text);
- }
+ using var stream = File.OpenRead("Test Data/example.srt");
+
+ var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt");
+ Assert.Equal(2, parsed.TrackEvents.Count);
+
+ var trackEvent1 = parsed.TrackEvents[0];
+ Assert.Equal("1", trackEvent1.Id);
+ Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
+ Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text);
+
+ var trackEvent2 = parsed.TrackEvents[1];
+ Assert.Equal("2", trackEvent2.Id);
+ Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
+ 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 SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt");
- 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);
- }
+ using var stream = File.OpenRead("Test Data/example2.srt");
+
+ var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt");
+ 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 6abf2d26c..5b7aa7eaa 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
@@ -18,22 +18,21 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
[MemberData(nameof(Parse_MultipleDialogues_TestData))]
public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents)
{
- using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
- {
- SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa");
+ using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa));
- Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
+ SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa");
- for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i)
- {
- SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i];
- SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i];
+ Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
+
+ for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i)
+ {
+ SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i];
+ SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i];
- Assert.Equal(expected.Id, actual.Id);
- Assert.Equal(expected.Text, actual.Text);
- Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks);
- Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks);
- }
+ Assert.Equal(expected.Id, actual.Id);
+ Assert.Equal(expected.Text, actual.Text);
+ Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks);
+ Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks);
}
}
@@ -73,17 +72,16 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
[Fact]
public void Parse_Valid_Success()
{
- using (var stream = File.OpenRead("Test Data/example.ssa"))
- {
- var parsed = _parser.Parse(stream, "ssa");
- Assert.Single(parsed.TrackEvents);
- var trackEvent = parsed.TrackEvents[0];
+ using var stream = File.OpenRead("Test Data/example.ssa");
- Assert.Equal("1", trackEvent.Id);
- Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
- Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
- Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text);
- }
+ var parsed = _parser.Parse(stream, "ssa");
+ Assert.Single(parsed.TrackEvents);
+ var trackEvent = parsed.TrackEvents[0];
+
+ Assert.Equal("1", trackEvent.Id);
+ Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
+ Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
index 9ace80bbd..ce1f005f4 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
@@ -97,7 +97,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
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);
+ var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None);
Assert.Equal(subtitleInfo.Path, result.Path);
Assert.Equal(subtitleInfo.Protocol, result.Protocol);
Assert.Equal(subtitleInfo.Format, result.Format);
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index c30dad6f9..210ce4a47 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -351,11 +351,11 @@ namespace Jellyfin.Model.Tests
// Assert.Contains(uri.Extension, containers);
// Check expected video codec (1)
- Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec);
+ Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec);
Assert.Single(streamInfo.TargetVideoCodec);
// Check expected audio codecs (1)
- Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec);
+ Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec);
Assert.Single(streamInfo.TargetAudioCodec);
// Assert.Single(val.AudioCodecs);
@@ -410,13 +410,13 @@ namespace Jellyfin.Model.Tests
else
{
// Check expected video codec (1)
- Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec);
+ Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec);
Assert.Single(streamInfo.TargetVideoCodec);
if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
{
// Check expected audio codecs (1)
- if (!targetAudioStream.IsExternal)
+ if (targetAudioStream?.IsExternal == false)
{
// Check expected audio codecs (1)
if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported))
@@ -432,7 +432,7 @@ namespace Jellyfin.Model.Tests
else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
{
// Check expected audio codecs (1)
- Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs);
+ Assert.Contains(targetAudioStream?.Codec, streamInfo.AudioCodecs);
Assert.Single(streamInfo.AudioCodecs);
}
@@ -440,10 +440,10 @@ namespace Jellyfin.Model.Tests
var videoStream = targetVideoStream;
Assert.False(streamInfo.EstimateContentLength);
Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo);
- Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>());
- Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel);
- Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth);
- Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
+ Assert.Contains(videoStream?.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>());
+ Assert.Equal(videoStream?.Level, streamInfo.TargetVideoLevel);
+ Assert.Equal(videoStream?.BitDepth, streamInfo.TargetVideoBitDepth);
+ Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream?.BitRate.GetValueOrDefault() ?? 0, int.MaxValue);
// Audio codec not supported
if ((why & TranscodeReason.AudioCodecNotSupported) != 0)
@@ -452,7 +452,7 @@ namespace Jellyfin.Model.Tests
if (options.AudioStreamIndex >= 0)
{
// TODO:fixme
- if (!targetAudioStream.IsExternal)
+ if (targetAudioStream?.IsExternal == false)
{
Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
}
@@ -488,16 +488,16 @@ namespace Jellyfin.Model.Tests
private static async ValueTask<T> TestData<T>(string name)
{
var path = Path.Join("Test Data", typeof(T).Name + "-" + name + ".json");
- using (var stream = File.OpenRead(path))
- {
- var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options);
- if (value is not null)
- {
- return value;
- }
- throw new SerializationException("Invalid test data: " + name);
+ using var stream = File.OpenRead(path);
+
+ var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options);
+ if (value is not null)
+ {
+ return value;
}
+
+ throw new SerializationException("Invalid test data: " + name);
}
private StreamBuilder GetStreamBuilder()
diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
index a5bdb42d8..198ad5a27 100644
--- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
+++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
@@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests
[InlineData((ImageFormat)5)]
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
+
+ [Theory]
+ [MemberData(nameof(GetAllImageFormats))]
+ public static void GetExtension_Valid_Valid(ImageFormat format)
+ => Assert.Null(Record.Exception(() => format.GetExtension()));
+
+ [Theory]
+ [InlineData((ImageFormat)int.MinValue)]
+ [InlineData((ImageFormat)int.MaxValue)]
+ [InlineData((ImageFormat)(-1))]
+ [InlineData((ImageFormat)5)]
+ public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
+ => Assert.Throws<InvalidEnumArgumentException>(() => format.GetExtension());
}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index c72a3315e..9b9c1ec34 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -14,18 +14,18 @@ namespace Jellyfin.Naming.Tests.AudioBook
data.Add(
new AudioBookFileInfo(
- @"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
+ "/server/AudioBooks/Larry Potter/Larry Potter.mp3",
"mp3"));
data.Add(
new AudioBookFileInfo(
- @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
+ "/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
"ogg",
chapterNumber: 1));
data.Add(
new AudioBookFileInfo(
- @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
+ "/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
"mp3",
chapterNumber: 2,
partNumber: 3));
@@ -49,7 +49,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
[Fact]
public void Resolve_InvalidExtension()
{
- var result = new AudioBookResolver(_namingOptions).Resolve(@"/server/AudioBooks/Larry Potter/Larry Potter.mp9");
+ var result = new AudioBookResolver(_namingOptions).Resolve("/server/AudioBooks/Larry Potter/Larry Potter.mp9");
Assert.Null(result);
}
diff --git a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
index 97949adff..ba602b5d2 100644
--- a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
@@ -20,11 +20,11 @@ public class ExternalPathParserTests
var hindiCultureDto = new CultureDto("Hindi", "Hindi", "hi", new[] { "hin" });
var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
- localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("en.*", RegexOptions.IgnoreCase)))
.Returns(englishCultureDto);
- localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"fr.*", RegexOptions.IgnoreCase)))
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("fr.*", RegexOptions.IgnoreCase)))
.Returns(frenchCultureDto);
- localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"hi.*", RegexOptions.IgnoreCase)))
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("hi.*", RegexOptions.IgnoreCase)))
.Returns(hindiCultureDto);
_audioPathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Audio);
diff --git a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
index c9a295a4c..471616797 100644
--- a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs
@@ -12,34 +12,34 @@ namespace Jellyfin.Naming.Tests.Music
[InlineData("", false)]
[InlineData("C:/", false)]
[InlineData("/home/", false)]
- [InlineData(@"blah blah", false)]
- [InlineData(@"D:/music/weezer/03 Pinkerton", false)]
- [InlineData(@"D:/music/michael jackson/Bad (2012 Remaster)", false)]
- [InlineData(@"cd1", true)]
- [InlineData(@"disc18", true)]
- [InlineData(@"disk10", true)]
- [InlineData(@"vol7", true)]
- [InlineData(@"volume1", true)]
- [InlineData(@"cd 1", true)]
- [InlineData(@"disc 1", true)]
- [InlineData(@"disk 1", true)]
- [InlineData(@"disk", false)]
- [InlineData(@"disk ·", false)]
- [InlineData(@"disk a", false)]
- [InlineData(@"disk volume", false)]
- [InlineData(@"disc disc", false)]
- [InlineData(@"disk disc 6", false)]
- [InlineData(@"cd - 1", true)]
- [InlineData(@"disc- 1", true)]
- [InlineData(@"disk - 1", true)]
- [InlineData(@"Disc 01 (Hugo Wolf · 24 Lieder)", true)]
- [InlineData(@"Disc 04 (Encores and Folk Songs)", true)]
- [InlineData(@"Disc04 (Encores and Folk Songs)", true)]
- [InlineData(@"Disc 04(Encores and Folk Songs)", true)]
- [InlineData(@"Disc04(Encores and Folk Songs)", true)]
- [InlineData(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2", true)]
- [InlineData(@"[1985] Opportunities (Let's make lots of money) (1985)", false)]
- [InlineData(@"Blah 04(Encores and Folk Songs)", false)]
+ [InlineData("blah blah", false)]
+ [InlineData("D:/music/weezer/03 Pinkerton", false)]
+ [InlineData("D:/music/michael jackson/Bad (2012 Remaster)", false)]
+ [InlineData("cd1", true)]
+ [InlineData("disc18", true)]
+ [InlineData("disk10", true)]
+ [InlineData("vol7", true)]
+ [InlineData("volume1", true)]
+ [InlineData("cd 1", true)]
+ [InlineData("disc 1", true)]
+ [InlineData("disk 1", true)]
+ [InlineData("disk", false)]
+ [InlineData("disk ·", false)]
+ [InlineData("disk a", false)]
+ [InlineData("disk volume", false)]
+ [InlineData("disc disc", false)]
+ [InlineData("disk disc 6", false)]
+ [InlineData("cd - 1", true)]
+ [InlineData("disc- 1", true)]
+ [InlineData("disk - 1", true)]
+ [InlineData("Disc 01 (Hugo Wolf · 24 Lieder)", true)]
+ [InlineData("Disc 04 (Encores and Folk Songs)", true)]
+ [InlineData("Disc04 (Encores and Folk Songs)", true)]
+ [InlineData("Disc 04(Encores and Folk Songs)", true)]
+ [InlineData("Disc04(Encores and Folk Songs)", true)]
+ [InlineData("D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2", true)]
+ [InlineData("[1985] Opportunities (Let's make lots of money) (1985)", false)]
+ [InlineData("Blah 04(Encores and Folk Songs)", false)]
public void AlbumParser_MultidiscPath_Identifies(string path, bool result)
{
var parser = new AlbumParser(_namingOptions);
diff --git a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
index d0d3d8292..f2cd360e5 100644
--- a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs
@@ -9,11 +9,11 @@ namespace Jellyfin.Naming.Tests.TV
private readonly EpisodeResolver _resolver = new EpisodeResolver(new NamingOptions());
[Theory]
- [InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)]
- [InlineData(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)]
- [InlineData(@"/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20)]
- [InlineData(@"/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24)]
- [InlineData(@"/server/Jeopardy 2023 07 14 HDTV x264 AC3.mkv", "Jeopardy", 2023, 07, 14)]
+ [InlineData("/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)]
+ [InlineData("/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)]
+ [InlineData("/server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv", "james.corden", 2017, 04, 20)]
+ [InlineData("/server/ABC News 2018_03_24_19_00_00.mkv", "ABC News", 2018, 03, 24)]
+ [InlineData("/server/Jeopardy 2023 07 14 HDTV x264 AC3.mkv", "Jeopardy", 2023, 07, 14)]
// TODO: [InlineData(@"/server/anything_14.11.1996.mp4", "anything", 1996, 11, 14)]
// TODO: [InlineData(@"/server/A Daily Show - (2015-01-15) - Episode Name - [720p].mkv", "A Daily Show", 2015, 01, 15)]
// TODO: [InlineData(@"/server/Last Man Standing_KTLADT_2018_05_25_01_28_00.wtv", "Last Man Standing", 2018, 05, 25)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
index 1da5a30a8..1727b2247 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
@@ -9,16 +9,16 @@ namespace Jellyfin.Naming.Tests.TV
private readonly EpisodeResolver _resolver = new EpisodeResolver(new NamingOptions());
[Theory]
- [InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")]
- [InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")]
- [InlineData(2, @"The Simpsons/02.avi")]
- [InlineData(2, @"The Simpsons/02 - Ep Name.avi")]
- [InlineData(2, @"The Simpsons/02-Ep Name.avi")]
- [InlineData(2, @"The Simpsons/02.EpName.avi")]
- [InlineData(2, @"The Simpsons/The Simpsons - 02.avi")]
- [InlineData(2, @"The Simpsons/The Simpsons - 02 Ep Name.avi")]
- [InlineData(7, @"GJ Club (2013)/GJ Club - 07.mkv")]
- [InlineData(17, @"Case Closed (1996-2007)/Case Closed - 317.mkv")]
+ [InlineData(8, "The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")]
+ [InlineData(2, "The Simpsons/The Simpsons - 02 - Ep Name.avi")]
+ [InlineData(2, "The Simpsons/02.avi")]
+ [InlineData(2, "The Simpsons/02 - Ep Name.avi")]
+ [InlineData(2, "The Simpsons/02-Ep Name.avi")]
+ [InlineData(2, "The Simpsons/02.EpName.avi")]
+ [InlineData(2, "The Simpsons/The Simpsons - 02.avi")]
+ [InlineData(2, "The Simpsons/The Simpsons - 02 Ep Name.avi")]
+ [InlineData(7, "GJ Club (2013)/GJ Club - 07.mkv")]
+ [InlineData(17, "Case Closed (1996-2007)/Case Closed - 317.mkv")]
// TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi")]
// TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 Ep Name.avi")]
// TODO: [InlineData(7, @"Seinfeld/Seinfeld 0807 The Checks.avi")]
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
index 7604ddc80..5397f1371 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
@@ -13,10 +13,10 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("/media/Foo - S04E011", true, "Foo", 4, 11)]
[InlineData("/media/Foo/Foo s01x01", true, "Foo", 1, 1)]
[InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", true, "Foo (2019)", 4, 3)]
- [InlineData("D:\\media\\Foo\\Foo-S01E01", true, "Foo", 1, 1)]
- [InlineData("D:\\media\\Foo - S04E011", true, "Foo", 4, 11)]
- [InlineData("D:\\media\\Foo\\Foo s01x01", true, "Foo", 1, 1)]
- [InlineData("D:\\media\\Foo (2019)\\Season 4\\Foo (2019).S04E03", true, "Foo (2019)", 4, 3)]
+ [InlineData(@"D:\media\Foo\Foo-S01E01", true, "Foo", 1, 1)]
+ [InlineData(@"D:\media\Foo - S04E011", true, "Foo", 4, 11)]
+ [InlineData(@"D:\media\Foo\Foo s01x01", true, "Foo", 1, 1)]
+ [InlineData(@"D:\media\Foo (2019)\Season 4\Foo (2019).S04E03", true, "Foo (2019)", 4, 3)]
[InlineData("/Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", false, "Elementary", 2, 3)]
[InlineData("/Season 1/seriesname S01E02 blah.avi", false, "seriesname", 1, 2)]
[InlineData("/Running Man/Running Man S2017E368.mkv", false, "Running Man", 2017, 368)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
index ffaa64c3f..6d6591abf 100644
--- a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
@@ -9,66 +9,66 @@ namespace Jellyfin.Naming.Tests.TV
private readonly EpisodePathParser _episodePathParser = new EpisodePathParser(new NamingOptions());
[Theory]
- [InlineData(@"Season 1/4x01 – 20 Hours in America (1).mkv", null)]
- [InlineData(@"Season 1/01x02 blah.avi", null)]
- [InlineData(@"Season 1/S01x02 blah.avi", null)]
- [InlineData(@"Season 1/S01E02 blah.avi", null)]
- [InlineData(@"Season 1/S01xE02 blah.avi", null)]
- [InlineData(@"Season 1/seriesname 01x02 blah.avi", null)]
- [InlineData(@"Season 1/seriesname S01x02 blah.avi", null)]
- [InlineData(@"Season 1/seriesname S01E02 blah.avi", null)]
- [InlineData(@"Season 1/seriesname S01xE02 blah.avi", null)]
- [InlineData(@"Season 2/02x03 - 04 Ep Name.mp4", null)]
- [InlineData(@"Season 2/My show name 02x03 - 04 Ep Name.mp4", null)]
- [InlineData(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2/02x03-04-15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/02x03-E15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/02x03x04x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 26)]
- [InlineData(@"Season 1/S01E23-E24-E26 - The Woman.mp4", 26)]
+ [InlineData("Season 1/4x01 – 20 Hours in America (1).mkv", null)]
+ [InlineData("Season 1/01x02 blah.avi", null)]
+ [InlineData("Season 1/S01x02 blah.avi", null)]
+ [InlineData("Season 1/S01E02 blah.avi", null)]
+ [InlineData("Season 1/S01xE02 blah.avi", null)]
+ [InlineData("Season 1/seriesname 01x02 blah.avi", null)]
+ [InlineData("Season 1/seriesname S01x02 blah.avi", null)]
+ [InlineData("Season 1/seriesname S01E02 blah.avi", null)]
+ [InlineData("Season 1/seriesname S01xE02 blah.avi", null)]
+ [InlineData("Season 2/02x03 - 04 Ep Name.mp4", null)]
+ [InlineData("Season 2/My show name 02x03 - 04 Ep Name.mp4", null)]
+ [InlineData("Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2/02x03-04-15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/02x03-E15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/02x03 - x04 - x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 26)]
+ [InlineData("Season 1/S01E23-E24-E26 - The Woman.mp4", 26)]
// Four Digits seasons
- [InlineData(@"Season 2009/2009x02 blah.avi", null)]
- [InlineData(@"Season 2009/S2009x02 blah.avi", null)]
- [InlineData(@"Season 2009/S2009E02 blah.avi", null)]
- [InlineData(@"Season 2009/S2009xE02 blah.avi", null)]
- [InlineData(@"Season 2009/seriesname 2009x02 blah.avi", null)]
- [InlineData(@"Season 2009/seriesname S2009x02 blah.avi", null)]
- [InlineData(@"Season 2009/seriesname S2009E02 blah.avi", null)]
- [InlineData(@"Season 2009/seriesname S2009xE02 blah.avi", null)]
- [InlineData(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03-04-15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03-E15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/2009x03x04x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 15)]
- [InlineData(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 26)]
- [InlineData(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4", 26)]
+ [InlineData("Season 2009/2009x02 blah.avi", null)]
+ [InlineData("Season 2009/S2009x02 blah.avi", null)]
+ [InlineData("Season 2009/S2009E02 blah.avi", null)]
+ [InlineData("Season 2009/S2009xE02 blah.avi", null)]
+ [InlineData("Season 2009/seriesname 2009x02 blah.avi", null)]
+ [InlineData("Season 2009/seriesname S2009x02 blah.avi", null)]
+ [InlineData("Season 2009/seriesname S2009E02 blah.avi", null)]
+ [InlineData("Season 2009/seriesname S2009xE02 blah.avi", null)]
+ [InlineData("Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03-04-15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03-E15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/2009x03x04x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 15)]
+ [InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 26)]
+ [InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 26)]
// Without season number
- [InlineData(@"Season 1/02 - blah.avi", null)]
- [InlineData(@"Season 2/02 - blah 14 blah.avi", null)]
- [InlineData(@"Season 1/02 - blah-02 a.avi", null)]
- [InlineData(@"Season 2/02.avi", null)]
- [InlineData(@"Season 1/02-03 - blah.avi", 3)]
- [InlineData(@"Season 2/02-04 - blah 14 blah.avi", 4)]
- [InlineData(@"Season 1/02-05 - blah-02 a.avi", 5)]
- [InlineData(@"Season 2/02-04.avi", 4)]
- [InlineData(@"Season 2 /[HorribleSubs] Hunter X Hunter - 136[720p].mkv", null)]
+ [InlineData("Season 1/02 - blah.avi", null)]
+ [InlineData("Season 2/02 - blah 14 blah.avi", null)]
+ [InlineData("Season 1/02 - blah-02 a.avi", null)]
+ [InlineData("Season 2/02.avi", null)]
+ [InlineData("Season 1/02-03 - blah.avi", 3)]
+ [InlineData("Season 2/02-04 - blah 14 blah.avi", 4)]
+ [InlineData("Season 1/02-05 - blah-02 a.avi", 5)]
+ [InlineData("Season 2/02-04.avi", 4)]
+ [InlineData("Season 2 /[HorribleSubs] Hunter X Hunter - 136[720p].mkv", null)]
// With format specification that must not be detected as ending episode number
- [InlineData(@"Season 1/series-s09e14-1080p.mkv", null)]
- [InlineData(@"Season 1/series-s09e14-720p.mkv", null)]
- [InlineData(@"Season 1/series-s09e14-720i.mkv", null)]
- [InlineData(@"Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)]
- [InlineData(@"Season 1/MOONLIGHTING_s01e01-e04", 4)]
+ [InlineData("Season 1/series-s09e14-1080p.mkv", null)]
+ [InlineData("Season 1/series-s09e14-720p.mkv", null)]
+ [InlineData("Season 1/series-s09e14-720i.mkv", null)]
+ [InlineData("Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)]
+ [InlineData("Season 1/MOONLIGHTING_s01e01-e04", 4)]
public void TestGetEndingEpisodeNumberFromFile(string filename, int? endingEpisodeNumber)
{
var result = _episodePathParser.Parse(filename, false);
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
index 55af33836..6773bbeb1 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
@@ -6,23 +6,23 @@ namespace Jellyfin.Naming.Tests.TV
public class SeasonFolderTests
{
[Theory]
- [InlineData(@"/Drive/Season 1", 1, true)]
- [InlineData(@"/Drive/Season 2", 2, true)]
- [InlineData(@"/Drive/Season 02", 2, true)]
- [InlineData(@"/Drive/Seinfeld/S02", 2, true)]
- [InlineData(@"/Drive/Seinfeld/2", 2, true)]
- [InlineData(@"/Drive/Season 2009", 2009, true)]
- [InlineData(@"/Drive/Season1", 1, true)]
- [InlineData(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
- [InlineData(@"/Drive/Season 7 (2016)", 7, false)]
- [InlineData(@"/Drive/Staffel 7 (2016)", 7, false)]
- [InlineData(@"/Drive/Stagione 7 (2016)", 7, false)]
- [InlineData(@"/Drive/Season (8)", null, false)]
- [InlineData(@"/Drive/3.Staffel", 3, false)]
- [InlineData(@"/Drive/s06e05", null, false)]
- [InlineData(@"/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
- [InlineData(@"/Drive/extras", 0, true)]
- [InlineData(@"/Drive/specials", 0, true)]
+ [InlineData("/Drive/Season 1", 1, true)]
+ [InlineData("/Drive/Season 2", 2, true)]
+ [InlineData("/Drive/Season 02", 2, true)]
+ [InlineData("/Drive/Seinfeld/S02", 2, true)]
+ [InlineData("/Drive/Seinfeld/2", 2, true)]
+ [InlineData("/Drive/Season 2009", 2009, true)]
+ [InlineData("/Drive/Season1", 1, true)]
+ [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
+ [InlineData("/Drive/Season 7 (2016)", 7, false)]
+ [InlineData("/Drive/Staffel 7 (2016)", 7, false)]
+ [InlineData("/Drive/Stagione 7 (2016)", 7, false)]
+ [InlineData("/Drive/Season (8)", null, false)]
+ [InlineData("/Drive/3.Staffel", 3, false)]
+ [InlineData("/Drive/s06e05", null, false)]
+ [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
+ [InlineData("/Drive/extras", 0, true)]
+ [InlineData("/Drive/specials", 0, true)]
public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
{
var result = SeasonPathParser.Parse(path, true, true);
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
index 58ec1b5d2..94a953de3 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
@@ -51,8 +51,8 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 2009)]
[InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 2009)]
[InlineData("Series/1-12 - The Woman.mp4", 1)]
- [InlineData(@"Running Man/Running Man S2017E368.mkv", 2017)]
- [InlineData(@"Case Closed (1996-2007)/Case Closed - 317.mkv", 3)]
+ [InlineData("Running Man/Running Man S2017E368.mkv", 2017)]
+ [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 3)]
// TODO: [InlineData(@"Seinfeld/Seinfeld 0807 The Checks.avi", 8)]
public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected)
{
diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
index fa46ecc3a..3721cd28c 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
@@ -21,8 +21,8 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("Series/4x12 - The Woman.mp4", "", 4, 12)]
[InlineData("Series/LA X, Pt. 1_s06e32.mp4", "LA X, Pt. 1", 6, 32)]
[InlineData("[Baz-Bar]Foo - [1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
- [InlineData(@"/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)]
- [InlineData(@"Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)]
+ [InlineData("/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)]
+ [InlineData("Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)]
[InlineData("[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken/[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken - 12 (NVENC H.265 1080p).mkv", "Tensura Nikki - Tensei Shitara Slime Datta Ken", null, 12)]
[InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
[InlineData("Series/4-12 - The Woman.mp4", "", 4, 12, 12)]
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
index b1141df47..62d60e5a4 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
@@ -10,34 +10,34 @@ namespace Jellyfin.Naming.Tests.Video
private readonly NamingOptions _namingOptions = new NamingOptions();
[Theory]
- [InlineData(@"The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013)]
- [InlineData(@"The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013)]
- [InlineData(@"The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013)]
- [InlineData(@"The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013)]
- [InlineData(@"300 (2006).mkv", "300", 2006)]
- [InlineData(@"d:/movies/300 (2006).mkv", "300", 2006)]
- [InlineData(@"300 2 (2006).mkv", "300 2", 2006)]
- [InlineData(@"300 - 2 (2006).mkv", "300 - 2", 2006)]
- [InlineData(@"300 2001 (2006).mkv", "300 2001", 2006)]
- [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013)]
- [InlineData(@"curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013)]
- [InlineData(@"/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006)]
- [InlineData(@"Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016)]
- [InlineData(@"The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013)]
- [InlineData(@"The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013)]
- [InlineData(@"The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013)]
- [InlineData(@"The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013)]
- [InlineData(@"300 (2006)", "300", 2006)]
- [InlineData(@"d:/movies/300 (2006)", "300", 2006)]
- [InlineData(@"300 2 (2006)", "300 2", 2006)]
- [InlineData(@"300 - 2 (2006)", "300 - 2", 2006)]
- [InlineData(@"300 2001 (2006)", "300 2001", 2006)]
- [InlineData(@"/server/Movies/300 (2007)/300 (2006)", "300", 2006)]
- [InlineData(@"/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006)]
- [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv", null)]
- [InlineData(@"American Psycho.mkv", "American Psycho.mkv", null)]
- [InlineData(@"[rec].mkv", "[rec].mkv", null)]
- [InlineData(@"St. Vincent (2014)", "St. Vincent", 2014)]
+ [InlineData("The Wolf of Wall Street (2013).mkv", "The Wolf of Wall Street", 2013)]
+ [InlineData("The Wolf of Wall Street 2 (2013).mkv", "The Wolf of Wall Street 2", 2013)]
+ [InlineData("The Wolf of Wall Street - 2 (2013).mkv", "The Wolf of Wall Street - 2", 2013)]
+ [InlineData("The Wolf of Wall Street 2001 (2013).mkv", "The Wolf of Wall Street 2001", 2013)]
+ [InlineData("300 (2006).mkv", "300", 2006)]
+ [InlineData("d:/movies/300 (2006).mkv", "300", 2006)]
+ [InlineData("300 2 (2006).mkv", "300 2", 2006)]
+ [InlineData("300 - 2 (2006).mkv", "300 - 2", 2006)]
+ [InlineData("300 2001 (2006).mkv", "300 2001", 2006)]
+ [InlineData("curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", "curse.of.chucky", 2013)]
+ [InlineData("curse.of.chucky.2013.stv.unrated.multi.2160p.bluray.x264-rough", "curse.of.chucky", 2013)]
+ [InlineData("/server/Movies/300 (2007)/300 (2006).bluray.disc", "300", 2006)]
+ [InlineData("Arrival.2016.2160p.Blu-Ray.HEVC.mkv", "Arrival", 2016)]
+ [InlineData("The Wolf of Wall Street (2013)", "The Wolf of Wall Street", 2013)]
+ [InlineData("The Wolf of Wall Street 2 (2013)", "The Wolf of Wall Street 2", 2013)]
+ [InlineData("The Wolf of Wall Street - 2 (2013)", "The Wolf of Wall Street - 2", 2013)]
+ [InlineData("The Wolf of Wall Street 2001 (2013)", "The Wolf of Wall Street 2001", 2013)]
+ [InlineData("300 (2006)", "300", 2006)]
+ [InlineData("d:/movies/300 (2006)", "300", 2006)]
+ [InlineData("300 2 (2006)", "300 2", 2006)]
+ [InlineData("300 - 2 (2006)", "300 - 2", 2006)]
+ [InlineData("300 2001 (2006)", "300 2001", 2006)]
+ [InlineData("/server/Movies/300 (2007)/300 (2006)", "300", 2006)]
+ [InlineData("/server/Movies/300 (2007)/300 (2006).mkv", "300", 2006)]
+ [InlineData("American.Psycho.mkv", "American.Psycho.mkv", null)]
+ [InlineData("American Psycho.mkv", "American Psycho.mkv", null)]
+ [InlineData("[rec].mkv", "[rec].mkv", null)]
+ [InlineData("St. Vincent (2014)", "St. Vincent", 2014)]
[InlineData("Super movie(2009).mp4", "Super movie", 2009)]
[InlineData("Drug War 2013.mp4", "Drug War", 2013)]
[InlineData("My Movie (1997) - GreatestReleaseGroup 2019.mp4", "My Movie", 1997)]
@@ -45,9 +45,9 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("First Man (2018) 1080p.mkv", "First Man", 2018)]
[InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)]
// FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)]
- [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
+ [InlineData("3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
[InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)]
- [InlineData(@"Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)]
+ [InlineData("Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)]
[InlineData("My Movie 2013.12.09", "My Movie 2013.12.09", null)]
[InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)]
[InlineData("My Movie 20131209", "My Movie 20131209", null)]
diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
index 511a014a6..fccce5bff 100644
--- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
@@ -22,7 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void Test3DName()
{
- var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions);
+ var result = VideoResolver.ResolveFile("C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions);
Assert.Equal("hsbs", result?.Format3D);
Assert.Equal("Oblivion", result?.Name);
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 294f11ee7..183ec8984 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -15,10 +15,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
};
var result = VideoListResolver.Resolve(
@@ -34,10 +34,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv",
- @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv",
+ "/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
};
var result = VideoListResolver.Resolve(
@@ -54,8 +54,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv",
- @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
+ "/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv",
+ "/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
};
var result = VideoListResolver.Resolve(
@@ -71,13 +71,13 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/M/Movie 1.mkv",
- @"/movies/M/Movie 2.mkv",
- @"/movies/M/Movie 3.mkv",
- @"/movies/M/Movie 4.mkv",
- @"/movies/M/Movie 5.mkv",
- @"/movies/M/Movie 6.mkv",
- @"/movies/M/Movie 7.mkv"
+ "/movies/M/Movie 1.mkv",
+ "/movies/M/Movie 2.mkv",
+ "/movies/M/Movie 3.mkv",
+ "/movies/M/Movie 4.mkv",
+ "/movies/M/Movie 5.mkv",
+ "/movies/M/Movie 6.mkv",
+ "/movies/M/Movie 7.mkv"
};
var result = VideoListResolver.Resolve(
@@ -93,14 +93,14 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Movie/Movie.mkv",
- @"/movies/Movie/Movie-2.mkv",
- @"/movies/Movie/Movie-3.mkv",
- @"/movies/Movie/Movie-4.mkv",
- @"/movies/Movie/Movie-5.mkv",
- @"/movies/Movie/Movie-6.mkv",
- @"/movies/Movie/Movie-7.mkv",
- @"/movies/Movie/Movie-8.mkv"
+ "/movies/Movie/Movie.mkv",
+ "/movies/Movie/Movie-2.mkv",
+ "/movies/Movie/Movie-3.mkv",
+ "/movies/Movie/Movie-4.mkv",
+ "/movies/Movie/Movie-5.mkv",
+ "/movies/Movie/Movie-6.mkv",
+ "/movies/Movie/Movie-7.mkv",
+ "/movies/Movie/Movie-8.mkv"
};
var result = VideoListResolver.Resolve(
@@ -116,15 +116,15 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Mo/Movie 1.mkv",
- @"/movies/Mo/Movie 2.mkv",
- @"/movies/Mo/Movie 3.mkv",
- @"/movies/Mo/Movie 4.mkv",
- @"/movies/Mo/Movie 5.mkv",
- @"/movies/Mo/Movie 6.mkv",
- @"/movies/Mo/Movie 7.mkv",
- @"/movies/Mo/Movie 8.mkv",
- @"/movies/Mo/Movie 9.mkv"
+ "/movies/Mo/Movie 1.mkv",
+ "/movies/Mo/Movie 2.mkv",
+ "/movies/Mo/Movie 3.mkv",
+ "/movies/Mo/Movie 4.mkv",
+ "/movies/Mo/Movie 5.mkv",
+ "/movies/Mo/Movie 6.mkv",
+ "/movies/Mo/Movie 7.mkv",
+ "/movies/Mo/Movie 8.mkv",
+ "/movies/Mo/Movie 9.mkv"
};
var result = VideoListResolver.Resolve(
@@ -140,11 +140,11 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Movie/Movie 1.mkv",
- @"/movies/Movie/Movie 2.mkv",
- @"/movies/Movie/Movie 3.mkv",
- @"/movies/Movie/Movie 4.mkv",
- @"/movies/Movie/Movie 5.mkv"
+ "/movies/Movie/Movie 1.mkv",
+ "/movies/Movie/Movie 2.mkv",
+ "/movies/Movie/Movie 3.mkv",
+ "/movies/Movie/Movie 4.mkv",
+ "/movies/Movie/Movie 5.mkv"
};
var result = VideoListResolver.Resolve(
@@ -162,11 +162,11 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
- @"/movies/Iron Man/Iron Man.mkv",
- @"/movies/Iron Man/Iron Man (2008).mkv",
- @"/movies/Iron Man/Iron Man (2009).mkv",
- @"/movies/Iron Man/Iron Man (2010).mkv",
- @"/movies/Iron Man/Iron Man (2011).mkv"
+ "/movies/Iron Man/Iron Man.mkv",
+ "/movies/Iron Man/Iron Man (2008).mkv",
+ "/movies/Iron Man/Iron Man (2009).mkv",
+ "/movies/Iron Man/Iron Man (2010).mkv",
+ "/movies/Iron Man/Iron Man (2011).mkv"
};
var result = VideoListResolver.Resolve(
@@ -182,13 +182,13 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Iron Man/Iron Man.mkv",
- @"/movies/Iron Man/Iron Man-720p.mkv",
- @"/movies/Iron Man/Iron Man-test.mkv",
- @"/movies/Iron Man/Iron Man-bluray.mkv",
- @"/movies/Iron Man/Iron Man-3d.mkv",
- @"/movies/Iron Man/Iron Man-3d-hsbs.mkv",
- @"/movies/Iron Man/Iron Man[test].mkv"
+ "/movies/Iron Man/Iron Man.mkv",
+ "/movies/Iron Man/Iron Man-720p.mkv",
+ "/movies/Iron Man/Iron Man-test.mkv",
+ "/movies/Iron Man/Iron Man-bluray.mkv",
+ "/movies/Iron Man/Iron Man-3d.mkv",
+ "/movies/Iron Man/Iron Man-3d-hsbs.mkv",
+ "/movies/Iron Man/Iron Man[test].mkv"
};
var result = VideoListResolver.Resolve(
@@ -211,13 +211,13 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Iron Man/Iron Man.mkv",
- @"/movies/Iron Man/Iron Man - 720p.mkv",
- @"/movies/Iron Man/Iron Man - test.mkv",
- @"/movies/Iron Man/Iron Man - bluray.mkv",
- @"/movies/Iron Man/Iron Man - 3d.mkv",
- @"/movies/Iron Man/Iron Man - 3d-hsbs.mkv",
- @"/movies/Iron Man/Iron Man [test].mkv"
+ "/movies/Iron Man/Iron Man.mkv",
+ "/movies/Iron Man/Iron Man - 720p.mkv",
+ "/movies/Iron Man/Iron Man - test.mkv",
+ "/movies/Iron Man/Iron Man - bluray.mkv",
+ "/movies/Iron Man/Iron Man - 3d.mkv",
+ "/movies/Iron Man/Iron Man - 3d-hsbs.mkv",
+ "/movies/Iron Man/Iron Man [test].mkv"
};
var result = VideoListResolver.Resolve(
@@ -240,8 +240,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Iron Man/Iron Man - B (2006).mkv",
- @"/movies/Iron Man/Iron Man - C (2007).mkv"
+ "/movies/Iron Man/Iron Man - B (2006).mkv",
+ "/movies/Iron Man/Iron Man - C (2007).mkv"
};
var result = VideoListResolver.Resolve(
@@ -256,13 +256,13 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Iron Man/Iron Man.mkv",
- @"/movies/Iron Man/Iron Man_720p.mkv",
- @"/movies/Iron Man/Iron Man_test.mkv",
- @"/movies/Iron Man/Iron Man_bluray.mkv",
- @"/movies/Iron Man/Iron Man_3d.mkv",
- @"/movies/Iron Man/Iron Man_3d-hsbs.mkv",
- @"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
+ "/movies/Iron Man/Iron Man.mkv",
+ "/movies/Iron Man/Iron Man_720p.mkv",
+ "/movies/Iron Man/Iron Man_test.mkv",
+ "/movies/Iron Man/Iron Man_bluray.mkv",
+ "/movies/Iron Man/Iron Man_3d.mkv",
+ "/movies/Iron Man/Iron Man_3d-hsbs.mkv",
+ "/movies/Iron Man/Iron Man_3d.hsbs.mkv"
};
var result = VideoListResolver.Resolve(
@@ -280,11 +280,11 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
- @"/movies/Iron Man/Iron Man (2007).mkv",
- @"/movies/Iron Man/Iron Man (2008).mkv",
- @"/movies/Iron Man/Iron Man (2009).mkv",
- @"/movies/Iron Man/Iron Man (2010).mkv",
- @"/movies/Iron Man/Iron Man (2011).mkv"
+ "/movies/Iron Man/Iron Man (2007).mkv",
+ "/movies/Iron Man/Iron Man (2008).mkv",
+ "/movies/Iron Man/Iron Man (2009).mkv",
+ "/movies/Iron Man/Iron Man (2010).mkv",
+ "/movies/Iron Man/Iron Man (2011).mkv"
};
var result = VideoListResolver.Resolve(
@@ -300,8 +300,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv",
- @"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
+ "/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv",
+ "/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
};
var result = VideoListResolver.Resolve(
@@ -317,8 +317,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
};
var result = VideoListResolver.Resolve(
@@ -334,12 +334,12 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv",
- @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv",
+ "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
};
var result = VideoListResolver.Resolve(
@@ -361,8 +361,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv",
- @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
+ "/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv",
+ "/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
};
var result = VideoListResolver.Resolve(
@@ -378,8 +378,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv",
- @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
+ "/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv",
+ "/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
};
var result = VideoListResolver.Resolve(
diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
index 97b52f749..c95703f53 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
@@ -384,8 +384,8 @@ namespace Jellyfin.Naming.Tests.Video
// No stacking here because there is no part/disc/etc
var files = new[]
{
- @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)",
- @"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
+ "M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)",
+ "M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
};
var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
index 1d50df7a6..fc852ae85 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
@@ -29,7 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestStubName()
{
- var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions);
+ var result = VideoResolver.ResolveFile("C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions);
Assert.Equal("Oblivion", result?.Name);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 0316377d4..377f82eac 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -200,8 +200,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1",
- @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
+ "M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1",
+ "M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
};
var result = VideoListResolver.Resolve(
@@ -217,8 +217,8 @@ namespace Jellyfin.Naming.Tests.Video
// These should be considered separate, unrelated videos
var files = new[]
{
- @"My movie #1.mp4",
- @"My movie #2.mp4"
+ "My movie #1.mp4",
+ "My movie #2.mp4"
};
var result = VideoListResolver.Resolve(
@@ -233,10 +233,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"No (2012) part1.mp4",
- @"No (2012) part2.mp4",
- @"No (2012) part1-trailer.mp4",
- @"No (2012)-trailer.mp4"
+ "No (2012) part1.mp4",
+ "No (2012) part2.mp4",
+ "No (2012) part1-trailer.mp4",
+ "No (2012)-trailer.mp4"
};
var result = VideoListResolver.Resolve(
@@ -254,10 +254,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/Movies/Top Gun (1984)/movie.mp4",
- @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
- @"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
- @"/Movies/trailer.mp4"
+ "/Movies/Top Gun (1984)/movie.mp4",
+ "/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
+ "/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
+ "/Movies/trailer.mp4"
};
var result = VideoListResolver.Resolve(
@@ -276,10 +276,10 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi",
- @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi",
- @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi",
- @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
+ "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi",
+ "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi",
+ "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi",
+ "/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
};
var result = VideoListResolver.Resolve(
@@ -294,7 +294,7 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
+ "/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
};
var result = VideoListResolver.Resolve(
@@ -309,7 +309,7 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"The Colony.mkv"
+ "The Colony.mkv"
};
var result = VideoListResolver.Resolve(
@@ -324,8 +324,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"Four Sisters and a Wedding - A.avi",
- @"Four Sisters and a Wedding - B.avi"
+ "Four Sisters and a Wedding - A.avi",
+ "Four Sisters and a Wedding - B.avi"
};
var result = VideoListResolver.Resolve(
@@ -342,8 +342,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"Four Rooms - A.avi",
- @"Four Rooms - A.mp4"
+ "Four Rooms - A.avi",
+ "Four Rooms - A.mp4"
};
var result = VideoListResolver.Resolve(
@@ -358,8 +358,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/Server/Despicable Me/Despicable Me (2010).mkv",
- @"/Server/Despicable Me/trailer.mkv"
+ "/Server/Despicable Me/Despicable Me (2010).mkv",
+ "/Server/Despicable Me/trailer.mkv"
};
var result = VideoListResolver.Resolve(
@@ -376,8 +376,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/Server/Despicable Me/Despicable Me (2010).mkv",
- @"/Server/Despicable Me/trailers/some title.mkv"
+ "/Server/Despicable Me/Despicable Me (2010).mkv",
+ "/Server/Despicable Me/trailers/some title.mkv"
};
var result = VideoListResolver.Resolve(
@@ -394,8 +394,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
- @"/Movies/Despicable Me/Despicable Me.mkv",
- @"/Movies/Despicable Me/trailers/trailer.mkv"
+ "/Movies/Despicable Me/Despicable Me.mkv",
+ "/Movies/Despicable Me/trailers/trailer.mkv"
};
var result = VideoListResolver.Resolve(
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index 33a99e107..8455a56a1 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -15,26 +15,26 @@ namespace Jellyfin.Naming.Tests.Video
var data = new TheoryData<VideoFileInfo>();
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
+ path: "/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
container: "mkv",
name: "7 Psychos"));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
+ path: "/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
container: "mkv",
name: "3 days to kill",
year: 2005));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/American Psycho/American.Psycho.mkv",
+ path: "/server/Movies/American Psycho/American.Psycho.mkv",
container: "mkv",
name: "American.Psycho"));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
+ path: "/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
container: "mkv",
name: "brave",
year: 2006,
@@ -43,14 +43,14 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
+ path: "/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
container: "mkv",
name: "300",
year: 2006));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
+ path: "/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
container: "mkv",
name: "300",
year: 2006,
@@ -59,7 +59,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
+ path: "/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
container: "disc",
name: "brave",
year: 2006,
@@ -68,7 +68,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
+ path: "/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
container: "disc",
name: "300",
year: 2006,
@@ -77,7 +77,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
+ path: "/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
container: "disc",
name: "Brave",
year: 2006,
@@ -86,7 +86,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
+ path: "/server/Movies/300 (2007)/300 (2006).bluray.disc",
container: "disc",
name: "300",
year: 2006,
@@ -95,7 +95,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
+ path: "/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
container: "mkv",
name: "300",
year: 2006,
@@ -103,7 +103,7 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
+ path: "/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
container: "mkv",
name: "Brave",
year: 2006,
@@ -111,28 +111,28 @@ namespace Jellyfin.Naming.Tests.Video
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/300 (2007)/300 (2006).mkv",
+ path: "/server/Movies/300 (2007)/300 (2006).mkv",
container: "mkv",
name: "300",
year: 2006));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
+ path: "/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
container: "mkv",
name: "Bad Boys",
year: 1995));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Brave (2007)/Brave (2006).mkv",
+ path: "/server/Movies/Brave (2007)/Brave (2006).mkv",
container: "mkv",
name: "Brave",
year: 2006));
data.Add(
new VideoFileInfo(
- path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4",
+ path: "/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4",
container: "mp4",
name: "Rain Man",
year: 1988));
@@ -174,8 +174,8 @@ namespace Jellyfin.Naming.Tests.Video
{
var paths = new[]
{
- @"/Server/Iron Man",
- @"Batman",
+ "/Server/Iron Man",
+ "Batman",
string.Empty
};
diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
index 2ff7c7de4..072e0a8c5 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
@@ -16,7 +16,6 @@ namespace Jellyfin.Networking.Tests
[InlineData("127.0.0.1:123")]
[InlineData("localhost")]
[InlineData("localhost:1345")]
- [InlineData("www.google.co.uk")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
index 0b07a3c53..2302f90b8 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
@@ -1,7 +1,9 @@
using System.Net;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
using Xunit;
namespace Jellyfin.Networking.Tests
@@ -28,7 +30,8 @@ namespace Jellyfin.Networking.Tests
LocalNetworkSubnets = network.Split(',')
};
- using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
Assert.True(networkManager.IsInLocalNetwork(ip));
}
@@ -56,9 +59,10 @@ namespace Jellyfin.Networking.Tests
LocalNetworkSubnets = network.Split(',')
};
- using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
- Assert.False(nm.IsInLocalNetwork(ip));
+ Assert.False(networkManager.IsInLocalNetwork(ip));
}
}
}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 731cbbafb..022b8a3d0 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -7,6 +7,7 @@ using Jellyfin.Networking.Extensions;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -54,7 +55,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = interfaces;
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
NetworkManager.MockNetworkSettings = string.Empty;
Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]");
@@ -200,7 +202,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
NetworkManager.MockNetworkSettings = string.Empty;
// Check to see if DNS resolution is working. If not, skip test.
@@ -229,24 +232,24 @@ namespace Jellyfin.Networking.Tests
[InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
// User on external network, we're bound internal and external - so result is override.
- [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+ [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "all=http://helloworld.com", "http://helloworld.com")]
// User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override.
- [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
+ [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "external=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
// User on internal network, no binding specified - so result is the 1st internal.
- [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
+ [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "external=http://helloworld.com", "eth16")]
// User on external network, internal binding only - so assumption is a proxy forward, return external override.
- [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+ [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=http://helloworld.com", "http://helloworld.com")]
// User on external network, no binding - so result is the 1st external which is overriden.
- [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+ [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=http://helloworld.com", "http://helloworld.com")]
- // User assumed to be internal, no binding - so result is the 1st internal.
- [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
+ // User assumed to be internal, no binding - so result is the 1st matching interface.
+ [InlineData("", "192.168.1.0/24", "", false, "all=http://helloworld.com", "eth16")]
- // User is internal, no binding - so result is the 1st internal, which is then overridden.
+ // User is internal, no binding - so result is the 1st internal interface, which is then overridden.
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
{
@@ -264,7 +267,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
NetworkManager.MockNetworkSettings = string.Empty;
if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null)
@@ -293,7 +297,9 @@ namespace Jellyfin.Networking.Tests
RemoteIPFilter = addresses.Split(','),
IsRemoteIPFilterBlacklist = false
};
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+ var startupConf = new Mock<IConfiguration>();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
}
@@ -314,7 +320,8 @@ namespace Jellyfin.Networking.Tests
IsRemoteIPFilterBlacklist = true
};
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
}
@@ -334,7 +341,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = interfaces;
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
var interfaceToUse = nm.GetBindAddress(string.Empty, out _);
@@ -358,7 +366,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = interfaces;
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
var interfaceToUse = nm.GetBindAddress(source, out _);
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index c12f0cd68..1263043a5 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -7,6 +7,9 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="AutoFixture" />
+ <PackageReference Include="AutoFixture.AutoMoq" />
+ <PackageReference Include="AutoFixture.Xunit2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit" />
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
index f157f01e5..d5ab6ab4b 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -352,11 +352,11 @@ namespace Jellyfin.Providers.Tests.Manager
{
if (forceRefresh)
{
- Assert.Matches(@"image url [0-9]", image.Path);
+ Assert.Matches("image url [0-9]", image.Path);
}
else
{
- Assert.DoesNotMatch(@"image url [0-9]", image.Path);
+ Assert.DoesNotMatch("image url [0-9]", image.Path);
}
}
}
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
index 400e30bd6..1e0851993 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
@@ -82,7 +82,7 @@ namespace Jellyfin.Providers.Tests.Manager
AddParts(providerManager, metadataServices: servicesList.Select(s => s.Object).ToArray());
var refreshOptions = new MetadataRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict));
- var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None).ConfigureAwait(false);
+ var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None);
Assert.Equal(ItemUpdateType.MetadataDownload, actual);
for (var i = 0; i < servicesList.Length; i++)
@@ -105,7 +105,7 @@ namespace Jellyfin.Providers.Tests.Manager
AddParts(providerManager, metadataServices: servicesList.Select(s => s.Object).ToArray());
var refreshOptions = new MetadataRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict));
- var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None).ConfigureAwait(false);
+ var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None);
var expectedResult = serviceFound ? ItemUpdateType.MetadataDownload : ItemUpdateType.None;
Assert.Equal(expectedResult, actual);
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
index 6b2d9021c..2bc686a33 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
@@ -98,9 +98,11 @@ namespace Jellyfin.Providers.Tests.MediaInfo
[InlineData(null, null, 1, ImageType.Primary, ImageFormat.Jpg)] // no label, finds primary
[InlineData("backdrop", null, 2, ImageType.Backdrop, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream
[InlineData("cover", null, 2, ImageType.Primary, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream
+ [InlineData(null, "bmp", 1, ImageType.Primary, ImageFormat.Bmp)]
+ [InlineData(null, "gif", 1, ImageType.Primary, ImageFormat.Gif)]
[InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)]
[InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)]
- [InlineData(null, "gif", 1, ImageType.Primary, ImageFormat.Gif)]
+ [InlineData(null, "webp", 1, ImageType.Primary, ImageFormat.Webp)]
public async void GetImage_Embedded_ReturnsCorrectSelection(string label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat)
{
var streams = new List<MediaStream>();
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
new file mode 100644
index 000000000..76922af8d
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
@@ -0,0 +1,61 @@
+using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Providers.MediaInfo;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo;
+
+public class FFProbeVideoInfoTests
+{
+ private readonly FFProbeVideoInfo _fFProbeVideoInfo;
+
+ public FFProbeVideoInfoTests()
+ {
+ var serverConfiguration = new ServerConfiguration()
+ {
+ DummyChapterDuration = (int)TimeSpan.FromMinutes(5).TotalSeconds
+ };
+ var serverConfig = new Mock<IServerConfigurationManager>();
+ serverConfig.Setup(c => c.Configuration)
+ .Returns(serverConfiguration);
+
+ IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ fixture.Inject(serverConfig);
+ _fFProbeVideoInfo = fixture.Create<FFProbeVideoInfo>();
+ }
+
+ [Theory]
+ [InlineData(-1L)]
+ [InlineData(long.MinValue)]
+ [InlineData(long.MaxValue)]
+ public void CreateDummyChapters_InvalidRuntime_ThrowsArgumentException(long? runtime)
+ {
+ Assert.Throws<ArgumentException>(
+ () => _fFProbeVideoInfo.CreateDummyChapters(new Video()
+ {
+ RunTimeTicks = runtime
+ }));
+ }
+
+ [Theory]
+ [InlineData(null, 0)]
+ [InlineData(0L, 0)]
+ [InlineData(1L, 0)]
+ [InlineData(TimeSpan.TicksPerMinute * 5, 0)]
+ [InlineData((TimeSpan.TicksPerMinute * 5) + 1, 1)]
+ [InlineData(TimeSpan.TicksPerMinute * 50, 10)]
+ public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount)
+ {
+ var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video()
+ {
+ RunTimeTicks = runtime
+ });
+
+ Assert.Equal(chaptersCount, chapters.Length);
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
index 6ee4b8ef2..2b3867512 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs
@@ -29,7 +29,7 @@ public class MediaInfoResolverTests
public const string VideoDirectoryPath = "Test Data/Video";
public const string VideoDirectoryRegex = @"Test Data[/\\]Video";
public const string MetadataDirectoryPath = "library/00/00000000000000000000000000000000";
- public const string MetadataDirectoryRegex = @"library.*";
+ public const string MetadataDirectoryRegex = "library.*";
private readonly ILocalizationManager _localizationManager;
private readonly MediaInfoResolver _subtitleResolver;
@@ -49,7 +49,7 @@ public class MediaInfoResolverTests
var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" });
var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
- localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
+ localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex("en.*", RegexOptions.IgnoreCase)))
.Returns(englishCultureDto);
_localizationManager = localizationManager.Object;
@@ -79,7 +79,7 @@ public class MediaInfoResolverTests
{
// need a media source manager capable of returning something other than file protocol
var mediaSourceManager = new Mock<IMediaSourceManager>();
- mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*")))
+ mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex("http.*")))
.Returns(MediaProtocol.Http);
BaseItem.MediaSourceManager = mediaSourceManager.Object;
@@ -186,7 +186,7 @@ public class MediaInfoResolverTests
{
// need a media source manager capable of returning something other than file protocol
var mediaSourceManager = new Mock<IMediaSourceManager>();
- mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex(@"http.*")))
+ mediaSourceManager.Setup(m => m.GetPathProtocol(It.IsRegex("http.*")))
.Returns(MediaProtocol.Http);
BaseItem.MediaSourceManager = mediaSourceManager.Object;
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
index 09eb22328..07061cfc7 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
@@ -31,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/music/Foo B.A.R./epic.flac", false)]
[InlineData("/media/music/Foo B.A.R", false)]
[InlineData("/media/music/Foo B.A.R.", false)]
+ [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index c33a957e6..1c35eb3f5 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -48,10 +48,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff/", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/jeff's band", "/home/not jeff", "/home/not jeff/consistently inconsistent.mp3")]
- [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
- [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")]
- [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")]
- [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")]
+ [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
+ [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")]
+ [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")]
+ [InlineData(@"C:\Users\jeff\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")]
[InlineData("/o", "/o", "/s", "/s")] // regression test for #5977
public void TryReplaceSubPath_ValidArgs_Correct(string path, string subPath, string newSubPath, string? expectedResult)
{
@@ -78,10 +78,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[Theory]
[InlineData(null, '/', null)]
[InlineData(null, '\\', null)]
- [InlineData("/home/jeff/myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")]
- [InlineData("C:\\Users\\Jeff\\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")]
- [InlineData("\\home/jeff\\myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")]
- [InlineData("\\home/jeff\\myfile.mkv", '/', "/home/jeff/myfile.mkv")]
+ [InlineData("/home/jeff/myfile.mkv", '\\', @"\home\jeff\myfile.mkv")]
+ [InlineData(@"C:\Users\Jeff\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")]
+ [InlineData(@"\home/jeff\myfile.mkv", '\\', @"\home\jeff\myfile.mkv")]
+ [InlineData(@"\home/jeff\myfile.mkv", '/', "/home/jeff/myfile.mkv")]
[InlineData("", '/', "")]
public void NormalizePath_SpecifyingSeparator_Normalizes(string path, char separator, string expectedPath)
{
@@ -90,8 +90,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[Theory]
[InlineData("/home/jeff/myfile.mkv")]
- [InlineData("C:\\Users\\Jeff\\myfile.mkv")]
- [InlineData("\\home/jeff\\myfile.mkv")]
+ [InlineData(@"C:\Users\Jeff\myfile.mkv")]
+ [InlineData(@"\home/jeff\myfile.mkv")]
public void NormalizePath_NoArgs_UsesDirectorySeparatorChar(string path)
{
var separator = Path.DirectorySeparatorChar;
@@ -101,8 +101,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[Theory]
[InlineData("/home/jeff/myfile.mkv", '/')]
- [InlineData("C:\\Users\\Jeff\\myfile.mkv", '\\')]
- [InlineData("\\home/jeff\\myfile.mkv", '/')]
+ [InlineData(@"C:\Users\Jeff\myfile.mkv", '\\')]
+ [InlineData(@"\home/jeff\myfile.mkv", '/')]
public void NormalizePath_OutVar_Correct(string path, char expectedSeparator)
{
var result = path.NormalizePath(out var separator);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
index c859d11c6..13ac3ddb0 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
@@ -52,7 +52,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
Url = "192.168.1.182"
};
- var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false);
+ var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None);
Assert.Equal("HDHomeRun PRIME", modelInfo.FriendlyName);
Assert.Equal("HDHR3-CC", modelInfo.ModelNumber);
Assert.Equal("hdhomerun3_cablecard", modelInfo.FirmwareName);
@@ -72,7 +72,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
Url = "10.10.10.100"
};
- var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false);
+ var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None);
Assert.Equal("HDHomeRun DUAL", modelInfo.FriendlyName);
Assert.Equal("HDHR3-US", modelInfo.ModelNumber);
Assert.Equal("hdhomerun3_atsc", modelInfo.FirmwareName);
@@ -103,7 +103,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
Url = "192.168.1.182"
};
- var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false);
+ var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None);
Assert.Equal(6, channels.Count);
Assert.Equal("4.1", channels[0].GuideNumber);
Assert.Equal("WCMH-DT", channels[0].GuideName);
@@ -133,7 +133,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
ImportFavoritesOnly = true
};
- var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false);
+ var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None);
Assert.Single(channels);
Assert.Equal("4.1", channels[0].GuideNumber);
Assert.Equal("WCMH-DT", channels[0].GuideName);
@@ -145,7 +145,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
[Fact]
public async Task TryGetTunerHostInfo_Valid_Success()
{
- var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None).ConfigureAwait(false);
+ var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None);
Assert.Equal(_hdHomerunHost.Type, host.Type);
Assert.Equal("192.168.1.182", host.Url);
Assert.Equal("HDHomeRun PRIME", host.FriendlyName);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
index e1d2bb2d5..d4f28f327 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
@@ -96,7 +96,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
var days = JsonSerializer.Deserialize<IReadOnlyList<DayDto>>(bytes, _jsonOptions);
Assert.NotNull(days);
- Assert.Equal(1, days!.Count);
+ Assert.Single(days);
var dayDto = days[0];
Assert.Equal("20454", dayDto.StationId);
@@ -110,7 +110,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
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.Single(dayDto.Programs[0].VideoProperties);
Assert.Equal("hdtv", dayDto.Programs[0].VideoProperties[0]);
}
@@ -126,13 +126,13 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
Assert.NotNull(programDtos);
Assert.Equal(2, programDtos!.Count);
Assert.Equal("EP000000060003", programDtos[0].ProgramId);
- Assert.Equal(1, programDtos[0].Titles.Count);
+ Assert.Single(programDtos[0].Titles);
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.Single(programDtos[0].Genres);
Assert.Equal("Sitcom", programDtos[0].Genres[0]);
Assert.Equal("The Poloceman Cometh", programDtos[0].EpisodeTitle150);
Assert.Equal(2, programDtos[0].Metadata[0].Gracenote?.Season);
@@ -161,7 +161,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
var showImagesDtos = JsonSerializer.Deserialize<IReadOnlyList<ShowImagesDto>>(bytes, _jsonOptions);
Assert.NotNull(showImagesDtos);
- Assert.Equal(1, showImagesDtos!.Count);
+ Assert.Single(showImagesDtos!);
Assert.Equal("SH00712240", showImagesDtos[0].ProgramId);
Assert.Equal(4, showImagesDtos[0].Data.Count);
Assert.Equal("135", showImagesDtos[0].Data[0].Width);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
index d4b90dac0..934024826 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
@@ -119,8 +119,8 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
[InlineData("C:\\some.dll")] // Windows root path.
[InlineData("test.txt")] // Not a DLL
[InlineData(".././.././../some.dll")] // Traversal with current and parent
- [InlineData("..\\.\\..\\.\\..\\some.dll")] // Windows traversal with current and parent
- [InlineData("\\\\network\\resource.dll")] // UNC Path
+ [InlineData(@"..\.\..\.\..\some.dll")] // Windows traversal with current and parent
+ [InlineData(@"\\network\resource.dll")] // UNC Path
[InlineData("https://jellyfin.org/some.dll")] // URL
[InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character.
public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath)
@@ -191,13 +191,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
- File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
+ await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
- var resultBytes = File.ReadAllBytes(metafilePath);
+ var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
@@ -231,7 +231,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var metafilePath = Path.Combine(_pluginPath, "meta.json");
- var resultBytes = File.ReadAllBytes(metafilePath);
+ var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
@@ -251,13 +251,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
- File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
+ await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
- var resultBytes = File.ReadAllBytes(metafilePath);
+ var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
@@ -277,13 +277,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
- File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options));
+ await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
- var resultBytes = File.ReadAllBytes(metafilePath);
+ var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs
new file mode 100644
index 000000000..ebd3a3891
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.SessionManager;
+
+public class SessionManagerTests
+{
+ [Theory]
+ [InlineData("", typeof(ArgumentException))]
+ [InlineData(null, typeof(ArgumentNullException))]
+ public async Task GetAuthorizationToken_Should_ThrowException(string deviceId, Type exceptionType)
+ {
+ await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager(
+ NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
+ Mock.Of<IEventManager>(),
+ Mock.Of<IUserDataManager>(),
+ Mock.Of<IServerConfigurationManager>(),
+ Mock.Of<ILibraryManager>(),
+ Mock.Of<IUserManager>(),
+ Mock.Of<IMusicManager>(),
+ Mock.Of<IDtoService>(),
+ Mock.Of<IImageProcessor>(),
+ Mock.Of<IServerApplicationHost>(),
+ Mock.Of<IDeviceManager>(),
+ Mock.Of<IMediaSourceManager>(),
+ Mock.Of<IHostApplicationLifetime>());
+
+ await Assert.ThrowsAsync(exceptionType, () => sessionManager.GetAuthorizationToken(
+ new User("test", "default", "default"),
+ deviceId,
+ "app_name",
+ "0.0.0",
+ "device_name"));
+ }
+
+ [Theory]
+ [MemberData(nameof(AuthenticateNewSessionInternal_Exception_TestData))]
+ public async Task AuthenticateNewSessionInternal_Should_ThrowException(AuthenticationRequest authenticationRequest, Type exceptionType)
+ {
+ await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager(
+ NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
+ Mock.Of<IEventManager>(),
+ Mock.Of<IUserDataManager>(),
+ Mock.Of<IServerConfigurationManager>(),
+ Mock.Of<ILibraryManager>(),
+ Mock.Of<IUserManager>(),
+ Mock.Of<IMusicManager>(),
+ Mock.Of<IDtoService>(),
+ Mock.Of<IImageProcessor>(),
+ Mock.Of<IServerApplicationHost>(),
+ Mock.Of<IDeviceManager>(),
+ Mock.Of<IMediaSourceManager>(),
+ Mock.Of<IHostApplicationLifetime>());
+
+ await Assert.ThrowsAsync(exceptionType, () => sessionManager.AuthenticateNewSessionInternal(authenticationRequest, false));
+ }
+
+ public static TheoryData<AuthenticationRequest, Type> AuthenticateNewSessionInternal_Exception_TestData()
+ {
+ var data = new TheoryData<AuthenticationRequest, Type>
+ {
+ {
+ new AuthenticationRequest { App = string.Empty, DeviceId = "device_id", DeviceName = "device_name", AppVersion = "app_version" },
+ typeof(ArgumentException)
+ },
+ {
+ new AuthenticationRequest { App = null, DeviceId = "device_id", DeviceName = "device_name", AppVersion = "app_version" },
+ typeof(ArgumentNullException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = string.Empty, DeviceName = "device_name", AppVersion = "app_version" },
+ typeof(ArgumentException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = null, DeviceName = "device_name", AppVersion = "app_version" },
+ typeof(ArgumentNullException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = string.Empty, AppVersion = "app_version" },
+ typeof(ArgumentException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = null, AppVersion = "app_version" },
+ typeof(ArgumentNullException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = "device_name", AppVersion = string.Empty },
+ typeof(ArgumentException)
+ },
+ {
+ new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = "device_name", AppVersion = null },
+ typeof(ArgumentNullException)
+ }
+ };
+
+ return data;
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
index 7abd2e685..5caf7d124 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
@@ -90,7 +90,7 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
Checksum = "InvalidChecksum"
};
- await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false);
+ await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None));
}
[Fact]
@@ -103,7 +103,7 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
Checksum = "11b5b2f1a9ebc4f66d6ef19018543361"
};
- var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false);
+ var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None));
Assert.Null(ex);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
index 3737fee0a..4e8aec9f1 100644
--- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -15,41 +15,41 @@ namespace Jellyfin.Server.Integration.Tests
{
public static class AuthHelper
{
- public const string AuthHeaderName = "X-Emby-Authorization";
- public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\"";
+ public const string AuthHeaderName = "Authorization";
+ public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\"";
public static async Task<string> CompleteStartupAsync(HttpClient client)
{
var jsonOptions = JsonDefaults.Options;
- var userResponse = await client.GetByteArrayAsync("/Startup/User").ConfigureAwait(false);
+ var userResponse = await client.GetByteArrayAsync("/Startup/User");
var user = JsonSerializer.Deserialize<StartupUserDto>(userResponse, jsonOptions);
- using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false);
+ using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
- using var content = JsonContent.Create(
+ using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName");
+ httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader);
+ httpRequest.Content = JsonContent.Create(
new AuthenticateUserByName()
{
Username = user!.Name,
Pw = user.Password,
},
options: jsonOptions);
- content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
- using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content).ConfigureAwait(false);
- var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
- await authResponse.Content.ReadAsStreamAsync().ConfigureAwait(false),
- jsonOptions).ConfigureAwait(false);
+ using var authResponse = await client.SendAsync(httpRequest);
+ authResponse.EnsureSuccessStatusCode();
+
+ var auth = await authResponse.Content.ReadFromJsonAsync<AuthenticationResultDto>(jsonOptions);
return auth!.AccessToken;
}
public static async Task<UserDto> GetUserDtoAsync(HttpClient client)
{
- using var response = await client.GetAsync("Users/Me").ConfigureAwait(false);
+ using var response = await client.GetAsync("Users/Me");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var userDto = await JsonSerializer.DeserializeAsync<UserDto>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false), JsonDefaults.Options).ConfigureAwait(false);
+ var userDto = await response.Content.ReadFromJsonAsync<UserDto>(JsonDefaults.Options);
Assert.NotNull(userDto);
return userDto;
}
@@ -58,15 +58,13 @@ namespace Jellyfin.Server.Integration.Tests
{
if (userId.Equals(default))
{
- var userDto = await GetUserDtoAsync(client).ConfigureAwait(false);
+ var userDto = await GetUserDtoAsync(client);
userId = userDto.Id;
}
- var response = await client.GetAsync($"Users/{userId}/Items/Root").ConfigureAwait(false);
+ var response = await client.GetAsync($"Users/{userId}/Items/Root");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
- JsonDefaults.Options).ConfigureAwait(false);
+ var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(JsonDefaults.Options);
Assert.NotNull(rootDto);
return rootDto;
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs
index be89fbc9a..96ca96558 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs
@@ -19,9 +19,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task ActivityLog_GetEntries_Ok()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync("System/ActivityLog/Entries").ConfigureAwait(false);
+ var response = await client.GetAsync("System/ActivityLog/Entries");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs
index 87136dfc8..8761cf69b 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs
@@ -1,4 +1,5 @@
using System.Net;
+using System.Net.Http.Json;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
@@ -30,8 +31,7 @@ namespace Jellyfin.Server.Integration.Tests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
- var responseBody = await response.Content.ReadAsStreamAsync();
- _ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody);
+ await response.Content.ReadFromJsonAsync<BrandingOptions>();
}
[Theory]
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
index 52df1cd60..39d449e27 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
@@ -1,5 +1,6 @@
using System.IO;
using System.Net;
+using System.Net.Http.Json;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
@@ -26,7 +27,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
{
var client = _factory.CreateClient();
- var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists").ConfigureAwait(false);
+ var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -36,12 +37,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
{
var client = _factory.CreateClient();
- var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false);
+ var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType);
StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!);
- Assert.Equal(await response.Content.ReadAsStringAsync().ConfigureAwait(false), await reader.ReadToEndAsync().ConfigureAwait(false));
+ Assert.Equal(await response.Content.ReadAsStringAsync(), await reader.ReadToEndAsync());
}
[Fact]
@@ -49,7 +50,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
{
var client = _factory.CreateClient();
- var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage").ConfigureAwait(false);
+ var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -58,14 +59,13 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task GetConfigurationPages_NoParams_AllConfigurationPages()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false);
+ var response = await client.GetAsync("/web/ConfigurationPages");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var res = await response.Content.ReadAsStreamAsync();
- _ = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions);
+ _ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOpions);
// TODO: check content
}
@@ -73,16 +73,15 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task GetConfigurationPages_True_MainMenuConfigurationPages()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false);
+ var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
- var res = await response.Content.ReadAsStreamAsync();
- var data = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions);
+ var data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOpions);
Assert.NotNull(data);
Assert.Empty(data);
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
index a65f65bb2..e5d5e785c 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
@@ -32,9 +32,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task GetProfile_DoesNotExist_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- using var response = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false);
+ using var response = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -43,9 +43,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task DeleteProfile_DoesNotExist_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- using var response = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false);
+ using var response = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -54,14 +54,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task UpdateProfile_DoesNotExist_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
var deviceProfile = new DeviceProfile()
{
Name = "ThisProfileDoesNotExist"
};
- using var response = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions).ConfigureAwait(false);
+ using var response = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -70,14 +70,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task CreateProfile_Valid_NoContent()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
var deviceProfile = new DeviceProfile()
{
Name = "ThisProfileIsNew"
};
- using var response = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions).ConfigureAwait(false);
+ using var response = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
@@ -86,16 +86,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task GetProfileInfos_Valid_ContainsThisProfileIsNew()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false);
+ using var response = await client.GetAsync("/Dlna/ProfileInfos");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
- var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
- _jsonOptions).ConfigureAwait(false);
+ var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions);
var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal));
Assert.NotNull(newProfile);
@@ -107,7 +105,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task UpdateProfile_Valid_NoContent()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
var updatedProfile = new DeviceProfile()
{
@@ -115,18 +113,16 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Id = _newDeviceProfileId
};
- using var postResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + _newDeviceProfileId, updatedProfile, _jsonOptions).ConfigureAwait(false);
+ using var postResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + _newDeviceProfileId, updatedProfile, _jsonOptions);
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
// Verify that the profile got updated
- using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false);
+ using var response = await client.GetAsync("/Dlna/ProfileInfos");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
- var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
- _jsonOptions).ConfigureAwait(false);
+ var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions);
Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal)));
var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal));
@@ -139,20 +135,18 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task DeleteProfile_Valid_NoContent()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- using var deleteResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId).ConfigureAwait(false);
+ using var deleteResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId);
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
// Verify that the profile got deleted
- using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false);
+ using var response = await client.GetAsync("/Dlna/ProfileInfos");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
- var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
- _jsonOptions).ConfigureAwait(false);
+ var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions);
Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal)));
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
index 078002994..23de2489e 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Net;
+using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
@@ -25,9 +26,9 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact
public async Task GetItems_NoApiKeyOrUserId_Success()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync("Items").ConfigureAwait(false);
+ var response = await client.GetAsync("Items");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
@@ -37,9 +38,9 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact
public async Task GetUserItems_NonExistentUserId_NotFound(string format)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -50,15 +51,13 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact
public async Task GetItems_UserId_Ok(string format)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client);
- var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)).ConfigureAwait(false);
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
- _jsonOptions).ConfigureAwait(false);
+ var items = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions);
Assert.NotNull(items);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
index 8998683a7..06abae14c 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
@@ -32,9 +32,9 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
public async Task Get_NonExistentItemId_NotFound(string format)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -45,7 +45,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
{
var client = _factory.CreateClient();
- var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+ var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()));
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
@@ -55,9 +55,9 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
public async Task Delete_NonExistentItemId_NotFound(string format)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+ var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs
index 34d26680a..abc8b6009 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs
@@ -20,9 +20,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task BitrateTest_Default_Ok()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync("Playback/BitrateTest").ConfigureAwait(false);
+ var response = await client.GetAsync("Playback/BitrateTest");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType);
@@ -34,9 +34,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task BitrateTest_WithValidParam_Ok(int size)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
+ var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType);
@@ -51,9 +51,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task BitrateTest_InvalidValue_BadRequest(int size)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
+ var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture));
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
index 24251013c..6699c6834 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
@@ -26,10 +26,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task RenameVirtualFolder_WhiteSpaceName_ReturnsBadRequest()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
using var postContent = new ByteArrayContent(Array.Empty<byte>());
- var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent).ConfigureAwait(false);
+ var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
@@ -38,10 +38,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task RenameVirtualFolder_WhiteSpaceNewName_ReturnsBadRequest()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
using var postContent = new ByteArrayContent(Array.Empty<byte>());
- var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent).ConfigureAwait(false);
+ var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
@@ -50,10 +50,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task RenameVirtualFolder_NameDoesntExist_ReturnsNotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
using var postContent = new ByteArrayContent(Array.Empty<byte>());
- var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent).ConfigureAwait(false);
+ var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -62,7 +62,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task AddMediaPath_PathDoesntExist_ReturnsNotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
var data = new MediaPathDto()
{
@@ -70,7 +70,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Path = "/this/path/doesnt/exist"
};
- var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions).ConfigureAwait(false);
+ var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -79,7 +79,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task UpdateMediaPath_WhiteSpaceName_ReturnsBadRequest()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
var data = new UpdateMediaPathRequestDto()
{
@@ -87,7 +87,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
PathInfo = new MediaPathInfo("test")
};
- var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions).ConfigureAwait(false);
+ var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
@@ -96,9 +96,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task RemoveMediaPath_WhiteSpaceName_ReturnsBadRequest()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+").ConfigureAwait(false);
+ var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
@@ -107,9 +107,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task RemoveMediaPath_PathDoesntExist_ReturnsNotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist").ConfigureAwait(false);
+ var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs
index 17f3dc99f..f9982cf12 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs
@@ -18,9 +18,9 @@ public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicatio
public async Task MusicGenres_FakeMusicGenre_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync("MusicGenres/Fake-MusicGenre").ConfigureAwait(false);
+ var response = await client.GetAsync("MusicGenres/Fake-MusicGenre");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs
new file mode 100644
index 000000000..38c64547c
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs
@@ -0,0 +1,26 @@
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public PersonsControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetPerson_DoesntExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ using var response = await client.GetAsync($"Persons/DoesntExist");
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
index 868ecd53f..9554d3ebc 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
@@ -19,9 +19,9 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false);
+ using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -29,9 +29,9 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
public async Task PostMarkPlayedItem_NonExistentUserId_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false);
+ using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -39,11 +39,11 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client);
- using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false);
+ using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -51,11 +51,11 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
public async Task PostMarkPlayedItem_NonExistentItemId_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client);
- using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false);
+ using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
index cb0a829e8..b9def13f8 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
@@ -19,9 +19,9 @@ public class SessionControllerTests : IClassFixture<JellyfinApplicationFactory>
public async Task GetSessions_NonExistentUserId_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}").ConfigureAwait(false);
+ using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
index 0dd22644a..36861294b 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
@@ -36,15 +36,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
PreferredMetadataLanguage = "nl"
};
- using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions).ConfigureAwait(false);
+ using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions);
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
- using var getResponse = await client.GetAsync("/Startup/Configuration").ConfigureAwait(false);
+ using var getResponse = await client.GetAsync("/Startup/Configuration");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType);
- using var responseStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions).ConfigureAwait(false);
+ var newConfig = await getResponse.Content.ReadFromJsonAsync<StartupConfigurationDto>(_jsonOptions);
Assert.Equal(config.UICulture, newConfig!.UICulture);
Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode);
Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage);
@@ -56,12 +55,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
{
var client = _factory.CreateClient();
- using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false);
+ using var response = await client.GetAsync("/Startup/User");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
- using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false);
+ var user = await response.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions);
Assert.NotNull(user);
Assert.NotNull(user.Name);
Assert.NotEmpty(user.Name);
@@ -80,15 +78,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Password = "NewPassword"
};
- var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions).ConfigureAwait(false);
+ var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions);
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
- var getResponse = await client.GetAsync("/Startup/User").ConfigureAwait(false);
+ var getResponse = await client.GetAsync("/Startup/User");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType);
- var contentStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false);
+ var newUser = await getResponse.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions);
Assert.NotNull(newUser);
Assert.Equal(user.Name, newUser.Name);
Assert.NotNull(newUser.Password);
@@ -102,7 +99,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
{
var client = _factory.CreateClient();
- var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false);
+ var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
@@ -112,7 +109,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
{
var client = _factory.CreateClient();
- using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false);
+ using var response = await client.GetAsync("/Startup/User");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
index 2a3c53dbe..4fcacd2ca 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
@@ -41,10 +41,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
{
var client = _factory.CreateClient();
- using var response = await client.GetAsync("Users/Public").ConfigureAwait(false);
+ using var response = await client.GetAsync("Users/Public");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOpions);
// User are hidden by default
Assert.NotNull(users);
Assert.Empty(users);
@@ -55,12 +54,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task GetUsers_Valid_Success()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- using var response = await client.GetAsync("Users").ConfigureAwait(false);
+ using var response = await client.GetAsync("Users");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOpions);
Assert.NotNull(users);
Assert.Single(users);
Assert.False(users![0].HasConfiguredPassword);
@@ -71,9 +69,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public async Task Me_Valid_Success()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- _ = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ _ = await AuthHelper.GetUserDtoAsync(client);
}
[Fact]
@@ -90,10 +88,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Name = TestUsername
};
- using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
+ using var response = await CreateUserByName(client, createRequest);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var user = await JsonSerializer.DeserializeAsync<UserDto>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOpions);
Assert.Equal(TestUsername, user!.Name);
Assert.False(user.HasPassword);
Assert.False(user.HasConfiguredPassword);
@@ -121,7 +118,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Name = username!
};
- using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
+ using var response = await CreateUserByName(client, createRequest);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
@@ -134,7 +131,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
// access token can't be null here as the previous test populated it
client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
- using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}").ConfigureAwait(false);
+ using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -150,11 +147,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
NewPw = "4randomPa$$word"
};
- using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false);
+ using var response = await UpdateUserPassword(client, _testUserId, createRequest);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
- await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ await client.GetStreamAsync("Users"), _jsonOpions);
var user = users!.First(x => x.Id.Equals(_testUserId));
Assert.True(user.HasPassword);
Assert.True(user.HasConfiguredPassword);
@@ -173,11 +170,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
CurrentPw = "4randomPa$$word",
};
- using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false);
+ using var response = await UpdateUserPassword(client, _testUserId, createRequest);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
- await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
+ await client.GetStreamAsync("Users"), _jsonOpions);
var user = users!.First(x => x.Id.Equals(_testUserId));
Assert.False(user.HasPassword);
Assert.False(user.HasConfiguredPassword);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
index 69f2ccf33..130281c6d 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Net;
+using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
@@ -25,9 +26,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
public async Task GetRootFolder_NonExistenUserId_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root").ConfigureAwait(false);
+ var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -35,9 +36,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
public async Task GetRootFolder_UserId_Valid()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- _ = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false);
+ _ = await AuthHelper.GetRootFolderDtoAsync(client);
}
[Theory]
@@ -49,11 +50,11 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
public async Task GetItem_NonExistenUserId_NotFound(string format)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client);
- var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)).ConfigureAwait(false);
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -66,11 +67,11 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
public async Task GetItem_NonExistentItemId_NotFound(string format)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client);
- var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())).ConfigureAwait(false);
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid()));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -78,16 +79,14 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
public async Task GetItem_UserIdAndItemId_Valid()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
- var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id);
- var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}").ConfigureAwait(false);
+ var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
- _jsonOptions).ConfigureAwait(false);
+ var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(_jsonOptions);
Assert.NotNull(rootDto);
}
@@ -95,16 +94,14 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
public async Task GetIntros_UserIdAndItemId_Valid()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
- var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id);
- var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros").ConfigureAwait(false);
+ var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
- _jsonOptions).ConfigureAwait(false);
+ var rootDto = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions);
Assert.NotNull(rootDto);
}
@@ -114,16 +111,14 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
public async Task LocalTrailersAndSpecialFeatures_UserIdAndItemId_Valid(string format)
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
- var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id);
- var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)).ConfigureAwait(false);
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>(
- await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
- _jsonOptions).ConfigureAwait(false);
+ var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto[]>(_jsonOptions);
Assert.NotNull(rootDto);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
index 0f9a2e90a..47bec5d79 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
@@ -19,9 +19,9 @@ public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFac
public async Task DeleteAlternateSources_NonExistentItemId_NotFound()
{
var client = _factory.CreateClient();
- client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
- var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}").ConfigureAwait(false);
+ var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
index 2361e4aa4..d2249cdc3 100644
--- a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
@@ -27,9 +27,9 @@ namespace Jellyfin.Server.Integration.Tests
{
var client = _factory.CreateClient();
- var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl).ConfigureAwait(false);
+ var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ string reply = await response.Content.ReadAsStringAsync();
Assert.Equal(unencodedUrl, reply);
}
@@ -40,9 +40,9 @@ namespace Jellyfin.Server.Integration.Tests
{
var client = _factory.CreateClient();
- var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl).ConfigureAwait(false);
+ var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ string reply = await response.Content.ReadAsStringAsync();
Assert.Equal(unencodedUrl, reply);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index 55bc43455..1c87d11f1 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
-using System.Threading;
using Emby.Server.Implementations;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
@@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests
var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
appHost.ServiceProvider = testServer.Services;
appHost.InitializeServices().GetAwaiter().GetResult();
- appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult();
+ appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
return testServer;
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs
index 8c49a2e2b..c8ad9d2a1 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Server.Integration.Tests.Middleware
AllowAutoRedirect = false
});
- var response = await client.GetAsync("robots.txt").ConfigureAwait(false);
+ var response = await client.GetAsync("robots.txt");
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("web/robots.txt", response.Headers.Location?.ToString());
diff --git a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
index 0ade345a1..98195a294 100644
--- a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs
@@ -31,10 +31,10 @@ namespace Jellyfin.Server.Integration.Tests
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
// Write out for publishing
- var responseBody = await response.Content.ReadAsStringAsync();
string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json"));
_outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath);
- File.WriteAllText(outputPath, responseBody);
+ await using var fs = File.Create(outputPath);
+ await response.Content.CopyToAsync(fs);
}
}
}
diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
index 49516cccc..288102037 100644
--- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
+++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
@@ -7,6 +7,7 @@ using Jellyfin.Server.Extensions;
using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -119,8 +120,8 @@ namespace Jellyfin.Server.Tests
EnableIPv6 = true,
EnableIPv4 = true,
};
-
- return new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+ var startupConf = new Mock<IConfiguration>();
+ return new NetworkManager(GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
}
}
}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index f63bc0e1b..c0d06116b 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Enums;
@@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
var item = result.Item;
- Assert.Equal("Rising (1)", item.Name);
+ Assert.Equal("Rising (1) / Rising (2)", item.Name);
Assert.Equal(1, item.IndexNumber);
Assert.Equal(2, item.IndexNumberEnd);
Assert.Equal(1, item.ParentIndexNumber);
- Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
+ Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview);
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
Assert.Equal(2004, item.ProductionYear);
}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index f56f58c6f..0a153b9cc 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -60,7 +60,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
{
Exists = true,
FullName = OperatingSystem.IsWindows() ?
- "C:\\media\\movies\\Justice League (2017).jpg"
+ @"C:\media\movies\Justice League (2017).jpg"
: "/media/movies/Justice League (2017).jpg"
};
directoryService.Setup(x => x.GetFile(_localImageFileMetadata.FullName))
diff --git a/tests/jellyfin-tests.ruleset b/tests/jellyfin-tests.ruleset
index e2abaf5bb..9d133da56 100644
--- a/tests/jellyfin-tests.ruleset
+++ b/tests/jellyfin-tests.ruleset
@@ -19,4 +19,10 @@
<!-- CA2234: Pass system uri objects instead of strings -->
<Rule Id="CA2234" Action="Info" />
</Rules>
+
+ <!-- xUnit -->
+ <Rules AnalyzerId="xUnit" RuleNamespace="xUnit">
+ <!-- Test methods must have a supported return type. -->
+ <Rule Id="xUnit1028" Action="None" />
+ </Rules>
</RuleSet>