aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Barron <barronpm@gmail.com>2021-06-18 18:47:44 -0400
committerPatrick Barron <barronpm@gmail.com>2021-06-18 18:56:10 -0400
commitbe88efce3cbbd357142a75f109258d6c7be398b4 (patch)
treedf8a0bf9d5e108d45a473edd8121a9af91ca6a0d
parent336ba2879f325a4efd52bc7737ce94f40369bfeb (diff)
parentc791c3a215b33bd4ca534c9f383c9fd7d23b59af (diff)
Merge branch 'master' into authenticationdb-efcore
# Conflicts: # Emby.Server.Implementations/Devices/DeviceManager.cs # Emby.Server.Implementations/HttpServer/Security/SessionContext.cs # Emby.Server.Implementations/Security/AuthenticationRepository.cs # Emby.Server.Implementations/Session/SessionManager.cs # Jellyfin.Server.Implementations/Security/AuthorizationContext.cs # MediaBrowser.Controller/Library/IUserManager.cs # MediaBrowser.Controller/Net/ISessionContext.cs
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md8
-rw-r--r--.github/label-commenter-config.yml43
-rw-r--r--.github/workflows/automation.yml26
-rw-r--r--.github/workflows/commands.yml119
-rw-r--r--.github/workflows/label-commenter.yml22
-rw-r--r--.github/workflows/merge-conflicts.yml17
-rw-r--r--.github/workflows/rebase.yml27
-rw-r--r--.gitignore1
-rw-r--r--CONTRIBUTORS.md4
-rw-r--r--Emby.Dlna/PlayTo/Device.cs36
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs41
-rw-r--r--Emby.Naming/TV/EpisodeResolver.cs5
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs2
-rw-r--r--Emby.Naming/Video/FlagParser.cs53
-rw-r--r--Emby.Naming/Video/Format3DParser.cs88
-rw-r--r--Emby.Naming/Video/Format3DResult.cs23
-rw-r--r--Emby.Naming/Video/StackResolver.cs4
-rw-r--r--Emby.Naming/Video/VideoFileInfo.cs7
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs233
-rw-r--r--Emby.Naming/Video/VideoResolver.cs60
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs8
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs46
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs5
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs2
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs2
-rw-r--r--Emby.Server.Implementations/Collections/CollectionImageProvider.cs4
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs2
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs2
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs2
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs2
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs6
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs34
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs523
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs4
-rw-r--r--Emby.Server.Implementations/Data/TypeMapper.cs20
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs2
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs2
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj8
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs11
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs2
-rw-r--r--Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs2
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs6
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs8
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs2
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs2
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs2
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs26
-rw-r--r--Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs2
-rw-r--r--Emby.Server.Implementations/IO/StreamHelper.cs2
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs1
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/DynamicImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/PlaylistImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs2
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs2
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs60
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs2
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs11
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs2
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs2
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs19
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs14
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs2
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs6
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs (renamed from Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs)46
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nn.json4
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs36
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs2
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs2
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs2
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs2
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs7
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs3
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs23
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs6
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs101
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs39
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs38
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs26
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs52
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs11
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs10
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs2
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs4
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumComparer.cs4
-rw-r--r--Emby.Server.Implementations/Sorting/ArtistComparer.cs4
-rw-r--r--Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/CriticRatingComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/DateCreatedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/IsFolderComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/IsPlayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/NameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/PremiereDateComparer.cs9
-rw-r--r--Emby.Server.Implementations/Sorting/ProductionYearComparer.cs9
-rw-r--r--Emby.Server.Implementations/Sorting/RandomComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs2
-rw-r--r--Emby.Server.Implementations/SyncPlay/Group.cs2
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs2
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs4
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs65
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs28
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs1
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs1
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs1
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs8
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs28
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/EventManager.cs9
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj8
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs2
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs10
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj4
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs32
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs35
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs91
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs11
-rw-r--r--Jellyfin.Server/Program.cs10
-rw-r--r--Jellyfin.Server/Startup.cs1
-rw-r--r--MediaBrowser.Common/Extensions/StringBuilderExtensions.cs35
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs8
-rw-r--r--MediaBrowser.Common/Updates/IInstallationManager.cs6
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs9
-rw-r--r--MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs12
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs9
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs3
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs3
-rw-r--r--MediaBrowser.Controller/Drawing/ImageHelper.cs3
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs9
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs74
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs3
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs51
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs6
-rw-r--r--MediaBrowser.Controller/Entities/IHasScreenshots.cs2
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs5
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChildComparer.cs3
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs7
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs3
-rw-r--r--MediaBrowser.Controller/Extensions/StringExtensions.cs21
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs13
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs11
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs8
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs6
-rw-r--r--MediaBrowser.Controller/Library/Profiler.cs85
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs7
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerEventInfo.cs3
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs (renamed from MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs)5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs27
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs21
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs1
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs9
-rw-r--r--MediaBrowser.Controller/Net/ISessionContext.cs4
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs8
-rw-r--r--MediaBrowser.Controller/Net/WebSocketListenerState.cs17
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs12
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs11
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs2
-rw-r--r--MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs2
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs21
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs2
-rw-r--r--MediaBrowser.Controller/Providers/ItemLookupInfo.cs6
-rw-r--r--MediaBrowser.Controller/Providers/MetadataResult.cs18
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs (renamed from MediaBrowser.Controller/Resolvers/BaseItemResolver.cs)2
-rw-r--r--MediaBrowser.Controller/Sorting/AlphanumComparator.cs2
-rw-r--r--MediaBrowser.Controller/Sorting/IBaseItemComparer.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs26
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs1
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs6
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs15
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs4
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj1
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaChapter.cs1
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs96
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/AssParser.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs10
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs66
-rw-r--r--MediaBrowser.Model/Configuration/PathSubstitution.cs2
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs13
-rw-r--r--MediaBrowser.Model/Extensions/StringHelper.cs3
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs7
-rw-r--r--MediaBrowser.Model/IO/IShortcutHandler.cs2
-rw-r--r--MediaBrowser.Model/IO/IStreamHelper.cs2
-rw-r--r--MediaBrowser.Model/MediaInfo/MediaInfo.cs2
-rw-r--r--MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs1
-rw-r--r--MediaBrowser.Model/Plugins/PluginInfo.cs2
-rw-r--r--MediaBrowser.Model/Plugins/PluginPageInfo.cs2
-rw-r--r--MediaBrowser.Model/Querying/EpisodeQuery.cs75
-rw-r--r--MediaBrowser.Model/Querying/MovieRecommendationQuery.cs47
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs6
-rw-r--r--MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs64
-rw-r--r--MediaBrowser.Model/Sync/SyncCategory.cs22
-rw-r--r--MediaBrowser.Model/Sync/SyncJob.cs135
-rw-r--r--MediaBrowser.Model/Sync/SyncJobStatus.cs15
-rw-r--r--MediaBrowser.Model/Sync/SyncTarget.cs20
-rw-r--r--MediaBrowser.Model/Tasks/ITaskTrigger.cs6
-rw-r--r--MediaBrowser.Model/Updates/PackageInfo.cs4
-rw-r--r--MediaBrowser.Model/Updates/VersionInfo.cs2
-rw-r--r--MediaBrowser.Model/Users/UserAction.cs24
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs42
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs38
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs6
-rw-r--r--MediaBrowser.Providers/Manager/ProviderUtils.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs5
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs14
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs25
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs7
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm642
-rw-r--r--deployment/Dockerfile.debian.armhf2
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.linux.amd64-musl2
-rw-r--r--deployment/Dockerfile.linux.arm642
-rw-r--r--deployment/Dockerfile.linux.armhf2
-rw-r--r--deployment/Dockerfile.macos2
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rw-r--r--fuzz/.gitignore1
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj18
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Program.cs32
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt1
-rwxr-xr-xfuzz/Emby.Server.Implementations.Fuzz/fuzz.sh11
-rw-r--r--fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj22
-rw-r--r--fuzz/Jellyfin.Server.Fuzz/Program.cs33
-rw-r--r--fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt1
-rwxr-xr-xfuzz/Jellyfin.Server.Fuzz/fuzz.sh11
-rw-r--r--jellyfin.ruleset3
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj4
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj3
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs33
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs15
-rw-r--r--tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs19
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj2
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs23
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json111
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json (renamed from tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/some_matadata.json)0
-rw-r--r--tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs14
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj3
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs6
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs7
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs9
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs208
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StubTests.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs242
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs32
-rw-r--r--tests/Jellyfin.Networking.Tests/IPHostTests.cs53
-rw-r--r--tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs49
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj3
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs60
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs49
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj3
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs31
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs14
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs53
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs48
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj4
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj4
-rw-r--r--tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs31
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs4
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs4
357 files changed, 3243 insertions, 2281 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d67e1c98b..12f1f5ed5 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -33,7 +33,13 @@ assignees: ''
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
-**Logs**
+**Server Logs**
+<!-- Please paste any log errors. -->
+
+**FFmpeg Logs**
+<!-- Please paste any log errors. -->
+
+**Browser Console Logs**
<!-- Please paste any log errors. -->
**Screenshots**
diff --git a/.github/label-commenter-config.yml b/.github/label-commenter-config.yml
deleted file mode 100644
index 78b75be43..000000000
--- a/.github/label-commenter-config.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-comment:
- header: Hello @{{ issue.user.login }}
- footer: "\
- ---\n\n
- > This is an automated comment created by the [peaceiris/actions-label-commenter]. \
- Responding to the bot or mentioning it won't have any effect.\n\n
- [peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter
- "
-
-labels:
- - name: stable backport
- labeled:
- pr:
- body: |
- This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release.
-
- Please observe the following:
-
- * Any dependent PRs that this PR requires **must** be tagged for stable backporting as well.
-
- * Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release.
-
- * This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided.
-
- To do this, run the following commands from your local copy of the Jellyfin repository:
-
- 1. `git checkout master`
-
- 1. `git merge --no-ff <myPullRequestBranch>`
-
- 1. `git log` -> `commit xxxxxxxxx`, grab hash
-
- 1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`)
-
- 1. `git cherry-pick -sx -m1 <hash>`
-
- Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff.
-
- Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly.
-
- **Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state.
-
- Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable.
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index de1590c74..8da2349c8 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/automation.yml
@@ -1,21 +1,31 @@
name: Automation
on:
- pull_request:
+ push:
+ branches:
+ - master
+ pull_request_target:
+ issue_comment:
jobs:
- main:
+ label:
+ name: Labeling
runs-on: ubuntu-latest
steps:
- - name: Does PR has the stable backport label?
- uses: Dreamcodeio/does-pr-has-label@v1.2
- id: checkLabel
+ - name: Apply label
+ uses: eps1lon/actions-label-merge-conflict@v2.0.1
+ if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
- label: stable backport
+ dirtyLabel: 'merge conflict'
+ repoToken: ${{ secrets.JF_BOT_TOKEN }}
+ project:
+ name: Project board
+ runs-on: ubuntu-latest
+ steps:
- name: Remove from 'Current Release' project
uses: alex-page/github-project-automation-plus@v0.7.1
- if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel
+ if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
project: Current Release
@@ -33,7 +43,7 @@ jobs:
- name: Add to 'Current Release' project
uses: alex-page/github-project-automation-plus@v0.7.1
- if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel
+ if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
project: Current Release
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
new file mode 100644
index 000000000..e0b91ecee
--- /dev/null
+++ b/.github/workflows/commands.yml
@@ -0,0 +1,119 @@
+name: Commands
+on:
+ issue_comment:
+ types:
+ - created
+ - edited
+ pull_request_target:
+ types:
+ - labeled
+ - synchronize
+
+jobs:
+ rebase:
+ name: Rebase
+ if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: '+1'
+
+ - name: Checkout the latest code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ fetch-depth: 0
+
+ - name: Automatic Rebase
+ uses: cirrus-actions/rebase@1.4
+ env:
+ GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+
+ check-backport:
+ name: Check Backport
+ if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Notify as seen
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ github.event.comment.id }}
+ reactions: eyes
+
+ - name: Checkout the latest code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ fetch-depth: 0
+
+ - name: Notify as running
+ id: comment_running
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ issue-number: ${{ github.event.issue.number }}
+ body: |
+ Running backport tests...
+
+ - name: Perform test backport
+ id: run_tests
+ run: |
+ set +o errexit
+ git config --global user.name "Jellyfin Bot"
+ git config --global user.email "team@jellyfin.org"
+ CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
+ git checkout master
+ git merge --no-ff ${CURRENT_BRANCH}
+ MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
+ git fetch --all
+ CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
+ stable_branch="Current stable release branch: ${CURRENT_STABLE}"
+ echo ${stable_branch}
+ echo ::set-output name=branch::${stable_branch}
+ git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
+ git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
+ retcode=$?
+ cat output.txt | grep -v 'hint:'
+ output="$( grep -v 'hint:' output.txt )"
+ output="${output//'%'/'%25'}"
+ output="${output//$'\n'/'%0A'}"
+ output="${output//$'\r'/'%0D'}"
+ echo ::set-output name=output::$output
+ exit ${retcode}
+
+ - name: Notify with result success
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null && success() }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ steps.comment_running.outputs.comment-id }}
+ body: |
+ ${{ steps.run_tests.outputs.branch }}
+ Output from `git cherry-pick`:
+
+ ---
+
+ ${{ steps.run_tests.outputs.output }}
+ reactions: hooray
+
+ - name: Notify with result failure
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ github.event.comment != null && failure() }}
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ comment-id: ${{ steps.comment_running.outputs.comment-id }}
+ body: |
+ ${{ steps.run_tests.outputs.branch }}
+ Output from `git cherry-pick`:
+
+ ---
+
+ ${{ steps.run_tests.outputs.output }}
+ reactions: confused
diff --git a/.github/workflows/label-commenter.yml b/.github/workflows/label-commenter.yml
deleted file mode 100644
index be9216cc1..000000000
--- a/.github/workflows/label-commenter.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: Label Commenter
-
-on:
- issues:
- types:
- - labeled
- - unlabeled
- pull_request_target:
- types:
- - labeled
- - unlabeled
-
-jobs:
- comment:
- runs-on: ubuntu-20.04
- steps:
- - uses: actions/checkout@v2
- with:
- ref: master
-
- - name: Label Commenter
- uses: peaceiris/actions-label-commenter@v1
diff --git a/.github/workflows/merge-conflicts.yml b/.github/workflows/merge-conflicts.yml
deleted file mode 100644
index 1b04eab46..000000000
--- a/.github/workflows/merge-conflicts.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: 'Merge Conflicts'
-
-on:
- push:
- branches:
- - master
- pull_request_target:
- types:
- - synchronize
-jobs:
- triage:
- runs-on: ubuntu-latest
- steps:
- - uses: eps1lon/actions-label-merge-conflict@v2.0.1
- with:
- dirtyLabel: 'merge conflict'
- repoToken: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml
deleted file mode 100644
index 3eec9fa03..000000000
--- a/.github/workflows/rebase.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: Automatic Rebase
-on:
- issue_comment:
-
-jobs:
- rebase:
- name: Rebase
- if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
- runs-on: ubuntu-latest
- steps:
- - name: Notify as seen
- uses: peter-evans/create-or-update-comment@v1.4.5
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- comment-id: ${{ github.event.comment.id }}
- reactions: '+1'
-
- - name: Checkout the latest code
- uses: actions/checkout@v2
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- fetch-depth: 0
-
- - name: Automatic Rebase
- uses: cirrus-actions/rebase@1.4
- env:
- GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 7cd3d0068..252210e57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -268,6 +268,7 @@ doc/
# Deployment artifacts
dist
*.exe
+*.dll
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 7a763a46c..b44961bf8 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -70,6 +70,7 @@
- [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro)
- [Matt07211](https://github.com/Matt07211)
+ - [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)
- [mitchfizz05](https://github.com/mitchfizz05)
- [MrTimscampi](https://github.com/MrTimscampi)
@@ -110,7 +111,7 @@
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
- - [ssenart] (https://github.com/ssenart)
+ - [ssenart](https://github.com/ssenart)
- [stanionascu](https://github.com/stanionascu)
- [stevehayles](https://github.com/stevehayles)
- [SuperSandro2000](https://github.com/SuperSandro2000)
@@ -146,6 +147,7 @@
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
+ - [peterspenler](https://github.com/peterspenler)
# Emby Contributors
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 5fa1fd589..6c580d15b 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -370,6 +370,42 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true);
}
+ /*
+ * SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
+ * Without that information, the next track command on the device does not work.
+ */
+ public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
+ {
+ var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
+
+ url = url.Replace("&", "&amp;", StringComparison.Ordinal);
+
+ _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
+
+ var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
+ if (command == null)
+ {
+ return;
+ }
+
+ var dictionary = new Dictionary<string, string>
+ {
+ { "NextURI", url },
+ { "NextURIMetaData", CreateDidlMeta(metaData) }
+ };
+
+ var service = GetAvTransportService();
+
+ if (service == null)
+ {
+ throw new InvalidOperationException("Unable to find service");
+ }
+
+ var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
private static string CreateDidlMeta(string value)
{
if (string.IsNullOrEmpty(value))
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 1e6a5fadb..0e49fd2c0 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -104,6 +104,22 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
+ /*
+ * Send a message to the DLNA device to notify what is the next track in the playlist.
+ */
+ private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
+ {
+ if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
+ {
+ // The current playing item is indeed in the play list and we are not yet at the end of the playlist.
+ var nextItemIndex = currentPlayListItemIndex + 1;
+ var nextItem = _playlist[nextItemIndex];
+
+ // Send the SetNextAvTransport message.
+ await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
private void OnDeviceUnavailable()
{
try
@@ -158,6 +174,15 @@ namespace Emby.Dlna.PlayTo
var newItemProgress = GetProgressInfo(streamInfo);
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
+
+ // Send a message to the DLNA device to notify what is the next track in the playlist.
+ var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
+ if (currentItemIndex >= 0)
+ {
+ _currentPlaylistIndex = currentItemIndex;
+ }
+
+ await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
}
catch (Exception ex)
{
@@ -427,6 +452,11 @@ namespace Emby.Dlna.PlayTo
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
+
+ // Send a message to the DLNA device to notify what is the next track in the play list.
+ var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+ await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
return;
}
@@ -625,6 +655,9 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
+ // Send a message to the DLNA device to notify what is the next track in the play list.
+ await SendNextTrackMessage(index, cancellationToken);
+
var streamInfo = currentitem.StreamInfo;
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
{
@@ -738,6 +771,10 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
+ // Send a message to the DLNA device to notify what is the next track in the play list.
+ var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+ await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
if (EnableClientSideSeek(newItem.StreamInfo))
{
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@@ -763,6 +800,10 @@ namespace Emby.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
+ // Send a message to the DLNA device to notify what is the next track in the play list.
+ var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+ await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
{
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index c63aec64e..5e952e47b 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -16,7 +16,7 @@ namespace Emby.Naming.TV
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
+ /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
public EpisodeResolver(NamingOptions options)
{
_options = options;
@@ -62,8 +62,7 @@ namespace Emby.Naming.TV
container = extension.TrimStart('.');
}
- var flags = new FlagParser(_options).GetFlags(path);
- var format3DResult = new Format3DParser(_options).Parse(flags);
+ var format3DResult = Format3DParser.Parse(path, _options);
var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index f9d06c09b..1fade985b 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -44,7 +44,7 @@ namespace Emby.Naming.Video
}
else if (rule.MediaType == MediaType.Video)
{
- if (!new VideoResolver(_options).IsVideoFile(path))
+ if (!VideoResolver.IsVideoFile(path, _options))
{
continue;
}
diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs
deleted file mode 100644
index 439de1813..000000000
--- a/Emby.Naming/Video/FlagParser.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.IO;
-using Emby.Naming.Common;
-
-namespace Emby.Naming.Video
-{
- /// <summary>
- /// Parses list of flags from filename based on delimiters.
- /// </summary>
- public class FlagParser
- {
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FlagParser"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
- public FlagParser(NamingOptions options)
- {
- _options = options;
- }
-
- /// <summary>
- /// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
- /// </summary>
- /// <param name="path">Path to file.</param>
- /// <returns>List of found flags.</returns>
- public string[] GetFlags(string path)
- {
- return GetFlags(path, _options.VideoFlagDelimiters);
- }
-
- /// <summary>
- /// Parses flags from filename based on delimiters.
- /// </summary>
- /// <param name="path">Path to file.</param>
- /// <param name="delimiters">Delimiters used to extract flags.</param>
- /// <returns>List of found flags.</returns>
- public string[] GetFlags(string path, char[] delimiters)
- {
- if (string.IsNullOrEmpty(path))
- {
- return Array.Empty<string>();
- }
-
- // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
-
- var file = Path.GetFileName(path);
-
- return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 4fd5d78ba..089089989 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/Emby.Naming/Video/Format3DParser.cs
@@ -1,45 +1,37 @@
using System;
-using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
- /// Parste 3D format related flags.
+ /// Parse 3D format related flags.
/// </summary>
- public class Format3DParser
+ public static class Format3DParser
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Format3DParser"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
- public Format3DParser(NamingOptions options)
- {
- _options = options;
- }
+ // Static default result to save on allocation costs.
+ private static readonly Format3DResult _defaultResult = new (false, null);
/// <summary>
/// Parse 3D format related flags.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
- public Format3DResult Parse(string path)
+ public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions)
{
- int oldLen = _options.VideoFlagDelimiters.Length;
- var delimiters = new char[oldLen + 1];
- _options.VideoFlagDelimiters.CopyTo(delimiters, 0);
+ int oldLen = namingOptions.VideoFlagDelimiters.Length;
+ Span<char> delimiters = stackalloc char[oldLen + 1];
+ namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
delimiters[oldLen] = ' ';
- return Parse(new FlagParser(_options).GetFlags(path, delimiters));
+ return Parse(path, delimiters, namingOptions);
}
- internal Format3DResult Parse(string[] videoFlags)
+ private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions)
{
- foreach (var rule in _options.Format3DRules)
+ foreach (var rule in namingOptions.Format3DRules)
{
- var result = Parse(videoFlags, rule);
+ var result = Parse(path, rule, delimiters);
if (result.Is3D)
{
@@ -47,51 +39,43 @@ namespace Emby.Naming.Video
}
}
- return new Format3DResult();
+ return _defaultResult;
}
- private static Format3DResult Parse(string[] videoFlags, Format3DRule rule)
+ private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters)
{
- var result = new Format3DResult();
+ bool is3D = false;
+ string? format3D = null;
- if (string.IsNullOrEmpty(rule.PrecedingToken))
+ // If there's no preceding token we just consider it found
+ var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
+ while (path.Length > 0)
{
- result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
- result.Is3D = !string.IsNullOrEmpty(result.Format3D);
-
- if (result.Is3D)
+ var index = path.IndexOfAny(delimiters);
+ if (index == -1)
{
- result.Tokens.Add(rule.Token);
+ index = path.Length - 1;
}
- }
- else
- {
- var foundPrefix = false;
- string? format = null;
- foreach (var flag in videoFlags)
- {
- if (foundPrefix)
- {
- result.Tokens.Add(rule.PrecedingToken);
+ var currentSlice = path[..index];
+ path = path[(index + 1)..];
- if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
- {
- format = flag;
- result.Tokens.Add(rule.Token);
- }
+ if (!foundPrefix)
+ {
+ foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+ continue;
+ }
- break;
- }
+ is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
- foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+ if (is3D)
+ {
+ format3D = rule.Token;
+ break;
}
-
- result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
- result.Format3D = format;
}
- return result;
+ return is3D ? new Format3DResult(true, format3D) : _defaultResult;
}
}
}
diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs
index ac935f203..aac959c13 100644
--- a/Emby.Naming/Video/Format3DResult.cs
+++ b/Emby.Naming/Video/Format3DResult.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace Emby.Naming.Video
{
/// <summary>
@@ -10,27 +8,24 @@ namespace Emby.Naming.Video
/// <summary>
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
/// </summary>
- public Format3DResult()
+ /// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param>
+ /// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param>
+ public Format3DResult(bool is3D, string? format3D)
{
- Tokens = new List<string>();
+ Is3D = is3D;
+ Format3D = format3D;
}
/// <summary>
- /// Gets or sets a value indicating whether [is3 d].
+ /// Gets a value indicating whether [is3 d].
/// </summary>
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
- public bool Is3D { get; set; }
+ public bool Is3D { get; }
/// <summary>
- /// Gets or sets the format3 d.
+ /// Gets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
- public string? Format3D { get; set; }
-
- /// <summary>
- /// Gets or sets the tokens.
- /// </summary>
- /// <value>The tokens.</value>
- public List<string> Tokens { get; set; }
+ public string? Format3D { get; }
}
}
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 550c42961..36f65a562 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -85,10 +85,8 @@ namespace Emby.Naming.Video
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{
- var resolver = new VideoResolver(_options);
-
var list = files
- .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
+ .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
.OrderBy(i => i.FullName)
.ToList();
diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs
index 1457db737..481773ff6 100644
--- a/Emby.Naming/Video/VideoFileInfo.cs
+++ b/Emby.Naming/Video/VideoFileInfo.cs
@@ -1,3 +1,4 @@
+using System;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
@@ -106,9 +107,9 @@ namespace Emby.Naming.Video
/// Gets the file name without extension.
/// </summary>
/// <value>The file name without extension.</value>
- public string FileNameWithoutExtension => !IsDirectory
- ? System.IO.Path.GetFileNameWithoutExtension(Path)
- : System.IO.Path.GetFileName(Path);
+ public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory
+ ? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
+ : System.IO.Path.GetFileName(Path.AsSpan());
/// <inheritdoc />
public override string ToString()
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 7b6a1705b..7da2dcd7a 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -12,31 +12,19 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
- public class VideoListResolver
+ public static class VideoListResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="VideoListResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
- public VideoListResolver(NamingOptions options)
- {
- _options = options;
- }
-
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="files">List of related video files.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
- public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
+ public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
{
- var videoResolver = new VideoResolver(_options);
-
var videoInfos = files
- .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
.OfType<VideoFileInfo>()
.ToList();
@@ -46,7 +34,7 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = new StackResolver(_options)
+ var stackResult = new StackResolver(namingOptions)
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos
@@ -59,23 +47,17 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
- var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
-
- var extras = GetExtras(remainingFiles, extraBaseNames);
+ var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
if (extras.Count > 0)
{
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
info.Extras = extras;
}
@@ -88,15 +70,12 @@ namespace Emby.Naming.Video
foreach (var media in standaloneMedia)
{
- var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } };
+ var info = new VideoInfo(media.Name) { Files = new[] { media } };
info.Year = info.Files[0].Year;
- var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
-
- remainingFiles = remainingFiles
- .Except(extras.Concat(new[] { media }))
- .ToList();
+ remainingFiles.Remove(media);
+ var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
info.Extras = extras;
@@ -105,8 +84,7 @@ namespace Emby.Naming.Video
if (supportMultiVersion)
{
- list = GetVideosGroupedByVersion(list)
- .ToList();
+ list = GetVideosGroupedByVersion(list, namingOptions);
}
// If there's only one resolved video, use the folder name as well to find extras
@@ -114,19 +92,14 @@ namespace Emby.Naming.Video
{
var info = list[0];
var videoPath = list[0].Files[0].Path;
- var parentPath = Path.GetDirectoryName(videoPath);
+ var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
- if (!string.IsNullOrEmpty(parentPath))
+ if (!parentPath.IsEmpty)
{
var folderName = Path.GetFileName(parentPath);
- if (!string.IsNullOrEmpty(folderName))
+ if (!folderName.IsEmpty)
{
- var extras = GetExtras(remainingFiles, new List<string> { folderName });
-
- remainingFiles = remainingFiles
- .Except(extras)
- .ToList();
-
+ var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
extras.AddRange(info.Extras);
info.Extras = extras;
}
@@ -164,96 +137,168 @@ namespace Emby.Naming.Video
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
- Files = new List<VideoFileInfo> { i },
+ Files = new[] { i },
Year = i.Year
}));
return list;
}
- private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
+ private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
{
if (videos.Count == 0)
{
return videos;
}
- var list = new List<VideoInfo>();
-
- var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
+ var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
- if (!string.IsNullOrEmpty(folderName)
- && folderName.Length > 1
- && videos.All(i => i.Files.Count == 1
- && IsEligibleForMultiVersion(folderName, i.Files[0].Path))
- && HaveSameYear(videos))
+ if (folderName.Length <= 1 || !HaveSameYear(videos))
{
- var ordered = videos.OrderBy(i => i.Name).ToList();
-
- list.Add(ordered[0]);
+ return videos;
+ }
- var alternateVersionsLen = ordered.Count - 1;
- var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- for (int i = 0; i < alternateVersionsLen; i++)
+ // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
+ for (var i = 0; i < videos.Count; i++)
+ {
+ var video = videos[i];
+ if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
- alternateVersions[i] = ordered[i + 1].Files[0];
+ return videos;
}
+ }
+
+ // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
+ videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
- list[0].AlternateVersions = alternateVersions;
- list[0].Name = folderName;
- var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
- extras.AddRange(list[0].Extras);
- list[0].Extras = extras;
+ var list = new List<VideoInfo>
+ {
+ videos[0]
+ };
- return list;
+ var alternateVersionsLen = videos.Count - 1;
+ var alternateVersions = new VideoFileInfo[alternateVersionsLen];
+ var extras = new List<VideoFileInfo>(list[0].Extras);
+ for (int i = 0; i < alternateVersionsLen; i++)
+ {
+ var video = videos[i + 1];
+ alternateVersions[i] = video.Files[0];
+ extras.AddRange(video.Extras);
}
- return videos;
- }
+ list[0].AlternateVersions = alternateVersions;
+ list[0].Name = folderName.ToString();
+ list[0].Extras = extras;
- private bool HaveSameYear(List<VideoInfo> videos)
- {
- return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
+ return list;
}
- private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
+ private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
{
- string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
- if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+ if (videos.Count == 1)
{
- // Remove the folder name before cleaning as we don't care about cleaning that part
- if (folderName.Length <= testFilename.Length)
- {
- testFilename = testFilename.Substring(folderName.Length).Trim();
- }
+ return true;
+ }
- if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
+ var firstYear = videos[0].Year ?? -1;
+ for (var i = 1; i < videos.Count; i++)
+ {
+ if ((videos[i].Year ?? -1) != firstYear)
{
- testFilename = cleanName.Trim().ToString();
+ return false;
}
+ }
- // The CleanStringParser should have removed common keywords etc.
- return string.IsNullOrEmpty(testFilename)
- || testFilename[0] == '-'
- || Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
+ return true;
+ }
+
+ private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
+ {
+ var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
+ if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
}
- return false;
+ // Remove the folder name before cleaning as we don't care about cleaning that part
+ if (folderName.Length <= testFilename.Length)
+ {
+ testFilename = testFilename[folderName.Length..].Trim();
+ }
+
+ // There are no span overloads for regex unfortunately
+ var tmpTestFilename = testFilename.ToString();
+ if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
+ {
+ tmpTestFilename = cleanName.Trim().ToString();
+ }
+
+ // The CleanStringParser should have removed common keywords etc.
+ return string.IsNullOrEmpty(tmpTestFilename)
+ || testFilename[0] == '-'
+ || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+ }
+
+ private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
}
- private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
+ private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
{
- foreach (var name in baseNames.ToList())
+ if (baseName.IsEmpty)
{
- var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
- baseNames.Add(trimmedName);
+ return false;
}
- return remainingFiles
- .Where(i => i.ExtraType != null)
- .Where(i => baseNames.Any(b =>
- i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
- .ToList();
+ return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
+ || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ /// <summary>
+ /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
+ /// </summary>
+ /// <param name="remainingFiles">The list of remaining filenames.</param>
+ /// <param name="baseName">The base name to use for the comparison.</param>
+ /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+ /// <returns>A list of video extras for [baseName].</returns>
+ private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
+ }
+
+ /// <summary>
+ /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
+ /// </summary>
+ /// <param name="remainingFiles">The list of remaining filenames.</param>
+ /// <param name="firstBaseName">The first base name to use for the comparison.</param>
+ /// <param name="secondBaseName">The second base name to use for the comparison.</param>
+ /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+ /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
+ private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
+ var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
+
+ var result = new List<VideoFileInfo>();
+ for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
+ {
+ var file = remainingFiles[pos];
+ if (file.ExtraType == null)
+ {
+ continue;
+ }
+
+ var filename = file.FileNameWithoutExtension;
+ if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
+ || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
+ {
+ result.Add(file);
+ remainingFiles.RemoveAt(pos);
+ }
+ }
+
+ return result;
}
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 27e73208c..c4ac5fdc6 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -9,38 +9,28 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves <see cref="VideoFileInfo"/> from file path.
/// </summary>
- public class VideoResolver
+ public static class VideoResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="VideoResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
- /// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
- public VideoResolver(NamingOptions options)
- {
- _options = options;
- }
-
/// <summary>
/// Resolves the directory.
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>VideoFileInfo.</returns>
- public VideoFileInfo? ResolveDirectory(string? path)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
{
- return Resolve(path, true);
+ return Resolve(path, true, namingOptions);
}
/// <summary>
/// Resolves the file.
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>VideoFileInfo.</returns>
- public VideoFileInfo? ResolveFile(string? path)
+ public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
{
- return Resolve(path, false);
+ return Resolve(path, false, namingOptions);
}
/// <summary>
@@ -48,10 +38,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
/// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
- public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
+ public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
{
if (string.IsNullOrEmpty(path))
{
@@ -67,10 +58,10 @@ namespace Emby.Naming.Video
var extension = Path.GetExtension(path.AsSpan());
// Check supported extensions
- if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
- if (!StubResolver.TryResolveFile(path, _options, out stubType))
+ if (!StubResolver.TryResolveFile(path, namingOptions, out stubType))
{
return null;
}
@@ -81,10 +72,9 @@ namespace Emby.Naming.Video
container = extension.TrimStart('.');
}
- var flags = new FlagParser(_options).GetFlags(path);
- var format3DResult = new Format3DParser(_options).Parse(flags);
+ var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
+ var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
var name = Path.GetFileNameWithoutExtension(path);
@@ -92,12 +82,12 @@ namespace Emby.Naming.Video
if (parseName)
{
- var cleanDateTimeResult = CleanDateTime(name);
+ var cleanDateTimeResult = CleanDateTime(name, namingOptions);
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
- && TryCleanString(name, out ReadOnlySpan<char> newName))
+ && TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
{
name = newName.ToString();
}
@@ -121,43 +111,47 @@ namespace Emby.Naming.Video
/// Determines if path is video file based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>True if is video file.</returns>
- public bool IsVideoFile(string path)
+ public static bool IsVideoFile(string path, NamingOptions namingOptions)
{
var extension = Path.GetExtension(path.AsSpan());
- return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
+ return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if path is video file stub based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>True if is video file stub.</returns>
- public bool IsStubFile(string path)
+ public static bool IsStubFile(string path, NamingOptions namingOptions)
{
var extension = Path.GetExtension(path.AsSpan());
- return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
+ return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Tries to clean name of clutter.
/// </summary>
/// <param name="name">Raw name.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
- public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
+ public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
{
- return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
+ return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
/// <summary>
/// Tries to get name and year from raw name.
/// </summary>
/// <param name="name">Raw name.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
- public CleanDateTimeResult CleanDateTime(string name)
+ public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions)
{
- return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
+ return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes);
}
}
}
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 660bbb2de..6edfad575 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
- DataPath = Path.Combine(ProgramDataPath, "data");
+ _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <summary>
@@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
- public string DataPath
- {
- get => _dataPath;
- private set => _dataPath = Directory.CreateDirectory(value).FullName;
- }
+ public string DataPath => _dataPath;
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 4f72c8ce1..d38535634 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
+ /// <summary>
+ /// The _configuration sync lock.
+ /// </summary>
+ private readonly object _configurationSyncLock = new object();
+
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -32,11 +39,6 @@ namespace Emby.Server.Implementations.AppBase
private bool _configurationLoaded;
/// <summary>
- /// The _configuration sync lock.
- /// </summary>
- private readonly object _configurationSyncLock = new object();
-
- /// <summary>
/// The _configuration.
/// </summary>
private BaseApplicationConfiguration _configuration;
@@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
/// <inheritdoc />
public object GetConfiguration(string key)
{
- return _configurations.GetOrAdd(key, k =>
- {
- var file = GetConfigurationFile(key);
+ return _configurations.GetOrAdd(
+ key,
+ (k, configurationManager) =>
+ {
+ var file = configurationManager.GetConfigurationFile(k);
- var configurationInfo = _configurationStores
- .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
+ var configurationInfo = Array.Find(
+ configurationManager._configurationStores,
+ i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
- if (configurationInfo == null)
- {
- throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
- }
+ if (configurationInfo == null)
+ {
+ throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
+ }
- var configurationType = configurationInfo.ConfigurationType;
+ var configurationType = configurationInfo.ConfigurationType;
- lock (_configurationSyncLock)
- {
- return LoadConfiguration(file, configurationType);
- }
- });
+ lock (configurationManager._configurationSyncLock)
+ {
+ return configurationManager.LoadConfiguration(file, configurationType);
+ }
+ },
+ this);
}
private object LoadConfiguration(string path, Type configurationType)
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 29bac6634..0308a68e4 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
@@ -35,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception)
{
- configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
+ // Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
+ configuration = Activator.CreateInstance(type)!;
}
using var stream = new MemoryStream(buffer?.Length ?? 0);
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index a69c4d035..baf5f88fc 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 7324b0ee9..448f12403 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Globalization;
diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
index c69a07e83..ca8409402 100644
--- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
+++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
@@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
return null;
})
.Where(i => i != null)
- .GroupBy(x => x.Id)
+ .GroupBy(x => x!.Id) // We removed the null values
.Select(x => x.First())
- .ToList();
+ .ToList()!; // Again... the list doesn't contain any null values
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index c56f33448..82d80fc83 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.IO;
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index 7a8ed8c29..ff5602f24 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Globalization;
using System.IO;
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 12a9e44e7..4a9b28085 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index c331a6112..6f23a0888 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
index 5c094ddd2..afc8966f9 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data
{
public class ManagedConnection : IDisposable
{
- private SQLiteDatabaseConnection _db;
+ private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
private bool _disposed = false;
@@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data
return _db.RunInTransaction(action, mode);
}
- public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql)
+ public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
{
return _db.Query(sql);
}
- public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values)
+ public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
{
return _db.Query(sql, values);
}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index a8f3feb58..3289e7609 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -1,9 +1,9 @@
+#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using SQLitePCL.pretty;
@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Data
});
}
- public static Guid ReadGuidFromBlob(this IResultSetValue result)
+ public static Guid ReadGuidFromBlob(this ResultSetValue result)
{
return new Guid(result.ToBlob());
}
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
- public static DateTime ReadDateTime(this IResultSetValue result)
+ public static DateTime ReadDateTime(this ResultSetValue result)
{
var dateText = result.ToString();
@@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Data
DateTimeStyles.None).ToUniversalTime();
}
- public static bool TryReadDateTime(this IReadOnlyList<IResultSetValue> reader, int index, out DateTime result)
+ public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
{
var item = reader[index];
if (item.IsDbNull())
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Data
return false;
}
- public static bool TryGetGuid(this IReadOnlyList<IResultSetValue> reader, int index, out Guid result)
+ public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
{
var item = reader[index];
if (item.IsDbNull())
@@ -131,17 +131,17 @@ namespace Emby.Server.Implementations.Data
return true;
}
- public static bool IsDbNull(this IResultSetValue result)
+ public static bool IsDbNull(this ResultSetValue result)
{
return result.SQLiteType == SQLiteType.Null;
}
- public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
+ public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToString();
}
- public static bool TryGetString(this IReadOnlyList<IResultSetValue> reader, int index, out string result)
+ public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
{
result = null;
var item = reader[index];
@@ -154,12 +154,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
+ public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToBool();
}
- public static bool TryGetBoolean(this IReadOnlyList<IResultSetValue> reader, int index, out bool result)
+ public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
{
var item = reader[index];
if (item.IsDbNull())
@@ -172,7 +172,7 @@ namespace Emby.Server.Implementations.Data
return true;
}
- public static bool TryGetInt32(this IReadOnlyList<IResultSetValue> reader, int index, out int result)
+ public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
{
var item = reader[index];
if (item.IsDbNull())
@@ -185,12 +185,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
+ public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToInt64();
}
- public static bool TryGetInt64(this IReadOnlyList<IResultSetValue> reader, int index, out long result)
+ public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
{
var item = reader[index];
if (item.IsDbNull())
@@ -203,7 +203,7 @@ namespace Emby.Server.Implementations.Data
return true;
}
- public static bool TryGetSingle(this IReadOnlyList<IResultSetValue> reader, int index, out float result)
+ public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
{
var item = reader[index];
if (item.IsDbNull())
@@ -216,7 +216,7 @@ namespace Emby.Server.Implementations.Data
return true;
}
- public static bool TryGetDouble(this IReadOnlyList<IResultSetValue> reader, int index, out double result)
+ public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
{
var item = reader[index];
if (item.IsDbNull())
@@ -229,7 +229,7 @@ namespace Emby.Server.Implementations.Data
return true;
}
- public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)
+ public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ReadGuidFromBlob();
}
@@ -441,7 +441,7 @@ namespace Emby.Server.Implementations.Data
}
}
- public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
+ public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
{
while (statement.MoveNext())
{
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index b3d8860a9..9b147b5d7 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -41,6 +43,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
{
+ private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
private readonly IServerConfigurationManager _config;
@@ -1043,18 +1046,34 @@ namespace Emby.Server.Implementations.Data
return Array.Empty<ItemImageInfo>();
}
- var list = new List<ItemImageInfo>();
- foreach (var part in value.SpanSplit('|'))
+ // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
+ var valueSpan = value.AsSpan();
+ var count = valueSpan.Count('|') + 1;
+
+ var position = 0;
+ var result = new ItemImageInfo[count];
+ foreach (var part in valueSpan.Split('|'))
{
var image = ItemImageInfoFromValueString(part);
if (image != null)
{
- list.Add(image);
+ result[position++] = image;
}
}
- return list.ToArray();
+ if (position == count)
+ {
+ return result;
+ }
+
+ if (position == 0)
+ {
+ return Array.Empty<ItemImageInfo>();
+ }
+
+ // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
+ return result[..position];
}
private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
@@ -1282,12 +1301,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- private BaseItem GetItem(IReadOnlyList<IResultSetValue> reader, InternalItemsQuery query)
+ private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query)
{
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query));
}
- private BaseItem GetItem(IReadOnlyList<IResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
+ private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
{
var typeString = reader.GetString(0);
@@ -1927,7 +1946,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<IResultSetValue> reader, BaseItem item)
+ private ChapterInfo GetChapter(IReadOnlyList<ResultSetValue> reader, BaseItem item)
{
var chapter = new ChapterInfo
{
@@ -2248,10 +2267,8 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
}
- private List<string> GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable<string> startColumns)
+ private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
{
- var list = startColumns.ToList();
-
foreach (var field in _allFields)
{
if (!HasField(query, field))
@@ -2259,28 +2276,28 @@ namespace Emby.Server.Implementations.Data
switch (field)
{
case ItemFields.Settings:
- list.Remove("IsLocked");
- list.Remove("PreferredMetadataCountryCode");
- list.Remove("PreferredMetadataLanguage");
- list.Remove("LockedFields");
+ columns.Remove("IsLocked");
+ columns.Remove("PreferredMetadataCountryCode");
+ columns.Remove("PreferredMetadataLanguage");
+ columns.Remove("LockedFields");
break;
case ItemFields.ServiceName:
- list.Remove("ExternalServiceId");
+ columns.Remove("ExternalServiceId");
break;
case ItemFields.SortName:
- list.Remove("ForcedSortName");
+ columns.Remove("ForcedSortName");
break;
case ItemFields.Taglines:
- list.Remove("Tagline");
+ columns.Remove("Tagline");
break;
case ItemFields.Tags:
- list.Remove("Tags");
+ columns.Remove("Tags");
break;
case ItemFields.IsHD:
// do nothing
break;
default:
- list.Remove(field.ToString());
+ columns.Remove(field.ToString());
break;
}
}
@@ -2288,60 +2305,60 @@ namespace Emby.Server.Implementations.Data
if (!HasProgramAttributes(query))
{
- list.Remove("IsMovie");
- list.Remove("IsSeries");
- list.Remove("EpisodeTitle");
- list.Remove("IsRepeat");
- list.Remove("ShowId");
+ columns.Remove("IsMovie");
+ columns.Remove("IsSeries");
+ columns.Remove("EpisodeTitle");
+ columns.Remove("IsRepeat");
+ columns.Remove("ShowId");
}
if (!HasEpisodeAttributes(query))
{
- list.Remove("SeasonName");
- list.Remove("SeasonId");
+ columns.Remove("SeasonName");
+ columns.Remove("SeasonId");
}
if (!HasStartDate(query))
{
- list.Remove("StartDate");
+ columns.Remove("StartDate");
}
if (!HasTrailerTypes(query))
{
- list.Remove("TrailerTypes");
+ columns.Remove("TrailerTypes");
}
if (!HasArtistFields(query))
{
- list.Remove("AlbumArtists");
- list.Remove("Artists");
+ columns.Remove("AlbumArtists");
+ columns.Remove("Artists");
}
if (!HasSeriesFields(query))
{
- list.Remove("SeriesId");
+ columns.Remove("SeriesId");
}
if (!HasEpisodeAttributes(query))
{
- list.Remove("SeasonName");
- list.Remove("SeasonId");
+ columns.Remove("SeasonName");
+ columns.Remove("SeasonId");
}
if (!query.DtoOptions.EnableImages)
{
- list.Remove("Images");
+ columns.Remove("Images");
}
if (EnableJoinUserData(query))
{
- list.Add("UserDatas.UserId");
- list.Add("UserDatas.lastPlayedDate");
- list.Add("UserDatas.playbackPositionTicks");
- list.Add("UserDatas.playcount");
- list.Add("UserDatas.isFavorite");
- list.Add("UserDatas.played");
- list.Add("UserDatas.rating");
+ columns.Add("UserDatas.UserId");
+ columns.Add("UserDatas.lastPlayedDate");
+ columns.Add("UserDatas.playbackPositionTicks");
+ columns.Add("UserDatas.playcount");
+ columns.Add("UserDatas.isFavorite");
+ columns.Add("UserDatas.played");
+ columns.Add("UserDatas.rating");
}
if (query.SimilarTo != null)
@@ -2389,7 +2406,7 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SimilarityScore");
- list.Add(builder.ToString());
+ columns.Add(builder.ToString());
var oldLen = query.ExcludeItemIds.Length;
var newLen = oldLen + item.ExtraIds.Length + 1;
@@ -2416,10 +2433,8 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SearchScore");
- list.Add(builder.ToString());
+ columns.Add(builder.ToString());
}
-
- return list;
}
private void BindSearchParams(InternalItemsQuery query, IStatement statement)
@@ -2485,31 +2500,25 @@ namespace Emby.Server.Implementations.Data
private string GetGroupBy(InternalItemsQuery query)
{
- var groups = new List<string>();
-
- if (EnableGroupByPresentationUniqueKey(query))
+ var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query);
+ if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey)
{
- groups.Add("PresentationUniqueKey");
+ return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey";
}
- if (query.GroupBySeriesPresentationUniqueKey)
+ if (enableGroupByPresentationUniqueKey)
{
- groups.Add("SeriesPresentationUniqueKey");
+ return " Group by PresentationUniqueKey";
}
- if (groups.Count > 0)
+ if (query.GroupBySeriesPresentationUniqueKey)
{
- return " Group by " + string.Join(',', groups);
+ return " Group by SeriesPresentationUniqueKey";
}
return string.Empty;
}
- private string GetFromText(string alias = "A")
- {
- return " from TypedBaseItems " + alias;
- }
-
public int GetCount(InternalItemsQuery query)
{
if (query == null)
@@ -2527,17 +2536,21 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = new List<string> { "count(distinct PresentationUniqueKey)" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 256)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
+ var commandText = commandTextBuilder.ToString();
int count;
using (var connection = GetConnection(true))
{
@@ -2579,20 +2592,23 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = _retriveItemColumns.ToList();
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 1024)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -2600,15 +2616,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
+ var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>();
using (var connection = GetConnection(true))
{
@@ -2764,20 +2783,27 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = _retriveItemColumns.ToList();
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 512)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
var whereText = whereClauses.Count == 0 ?
string.Empty :
- " where " + string.Join(" AND ", whereClauses);
+ string.Join(" AND ", whereClauses);
- commandText += whereText
- + GetGroupBy(query)
- + GetOrderByText(query);
+ if (!string.IsNullOrEmpty(whereText))
+ {
+ commandTextBuilder.Append(" where ")
+ .Append(whereText);
+ }
+
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -2785,43 +2811,58 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
- var statementTexts = new List<string>();
+ var itemQuery = string.Empty;
+ var totalRecordCountQuery = string.Empty;
if (!isReturningZeroItems)
{
- statementTexts.Add(commandText);
+ itemQuery = commandTextBuilder.ToString();
}
if (query.EnableTotalRecordCount)
{
- commandText = string.Empty;
+ commandTextBuilder.Clear();
+
+ commandTextBuilder.Append(" select ");
+ List<string> columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (guid)" };
}
- commandText += GetJoinUserDataText(query)
- + whereText;
- statementTexts.Add(commandText);
+ SetFinalColumnsToSelect(query, columnsToSelect);
+
+ commandTextBuilder.AppendJoin(',', columnsToSelect)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
+ if (!string.IsNullOrEmpty(whereText))
+ {
+ commandTextBuilder.Append(" where ")
+ .Append(whereText);
+ }
+
+ totalRecordCountQuery = commandTextBuilder.ToString();
}
var list = new List<BaseItem>();
@@ -2831,11 +2872,12 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts);
+ var itemQueryStatement = PrepareStatement(db, itemQuery);
+ var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
if (!isReturningZeroItems)
{
- using (var statement = statements[0])
+ using (var statement = itemQueryStatement)
{
if (EnableJoinUserData(query))
{
@@ -2865,11 +2907,14 @@ namespace Emby.Server.Implementations.Data
}
}
}
+
+ LogQueryTime("GetItems.ItemQuery", itemQuery, now);
}
+ now = DateTime.UtcNow;
if (query.EnableTotalRecordCount)
{
- using (var statement = statements[statements.Length - 1])
+ using (var statement = totalRecordCountQueryStatement)
{
if (EnableJoinUserData(query))
{
@@ -2884,11 +2929,12 @@ namespace Emby.Server.Implementations.Data
result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
}
+
+ LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now);
}
}, ReadTransactionMode);
}
- LogQueryTime("GetItems", commandText, now);
result.Items = list;
return result;
}
@@ -3021,19 +3067,22 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = new List<string> { "guid" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 256)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -3041,15 +3090,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
+ var commandText = commandTextBuilder.ToString();
var list = new List<Guid>();
using (var connection = GetConnection(true))
{
@@ -3088,7 +3140,9 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
+ var columns = new List<string> { "guid", "path" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandText = "select " + string.Join(',', columns) + FromText;
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
@@ -3164,9 +3218,11 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
+ var columns = new List<string> { "guid" };
+ SetFinalColumnsToSelect(query, columns);
var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
- + GetFromText()
+ + string.Join(',', columns)
+ + FromText
+ GetJoinUserDataText(query);
var whereClauses = GetWhereClauses(query, null);
@@ -3206,19 +3262,23 @@ namespace Emby.Server.Implementations.Data
{
commandText = string.Empty;
+ List<string> columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (guid)" };
}
+ SetFinalColumnsToSelect(query, columnsToSelect);
+ commandText += " select " + string.Join(',', columnsToSelect) + FromText;
+
commandText += GetJoinUserDataText(query)
+ whereText;
statementTexts.Add(commandText);
@@ -4413,56 +4473,50 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
}
- var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
- var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
-
var queryTopParentIds = query.TopParentIds;
- if (queryTopParentIds.Length == 1)
+ if (queryTopParentIds.Length > 0)
{
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
+ var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
+ var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
+
+ if (queryTopParentIds.Length == 1)
{
- whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
- if (statement != null)
+ if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
- statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
+ statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ }
+ else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ {
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
+ whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
+ }
+ else
+ {
+ whereClauses.Add("(TopParentId=@TopParentId)");
}
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
- }
- else
- {
- whereClauses.Add("(TopParentId=@TopParentId)");
- }
- if (statement != null)
- {
- statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
+ statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
}
- }
- else if (queryTopParentIds.Length > 1)
- {
- var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
-
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
+ else if (queryTopParentIds.Length > 1)
{
- whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
- if (statement != null)
+ var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+
+ if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
- statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
+ statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ }
+ else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ {
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
+ whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
+ }
+ else
+ {
+ whereClauses.Add("TopParentId in (" + val + ")");
}
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
- }
- else
- {
- whereClauses.Add("TopParentId in (" + val + ")");
}
}
@@ -4744,17 +4798,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- var types = new[]
- {
- nameof(Episode),
- nameof(Video),
- nameof(Movie),
- nameof(MusicVideo),
- nameof(Series),
- nameof(Season)
- };
-
- if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase))
{
return true;
}
@@ -5198,37 +5247,45 @@ AND Type = @InternalPersonType)");
var now = DateTime.UtcNow;
- var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
-
- var commandText = "Select Value From ItemValues where " + typeClause;
+ var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
+ if (itemValueTypes.Length == 1)
+ {
+ stringBuilder.Append('=')
+ .Append(itemValueTypes[0]);
+ }
+ else
+ {
+ stringBuilder.Append(" in (")
+ .AppendJoin(',', itemValueTypes)
+ .Append(')');
+ }
if (withItemTypes.Count > 0)
{
- var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'"));
- commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
+ stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (")
+ .AppendJoinInSingleQuotes(',', withItemTypes)
+ .Append("))");
}
if (excludeItemTypes.Count > 0)
{
- var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'"));
- commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
+ stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (")
+ .AppendJoinInSingleQuotes(',', excludeItemTypes)
+ .Append("))");
}
- commandText += " Group By CleanValue";
+ stringBuilder.Append(" Group By CleanValue");
+ var commandText = stringBuilder.ToString();
var list = new List<string>();
using (var connection = GetConnection(true))
+ using (var statement = PrepareStatement(connection, commandText))
{
- using (var statement = PrepareStatement(connection, commandText))
+ foreach (var row in statement.ExecuteQuery())
{
- foreach (var row in statement.ExecuteQuery())
+ if (row.TryGetString(0, out var result))
{
- if (row.TryGetString(0, out var result))
- {
- list.Add(result);
- }
+ list.Add(result);
}
}
}
@@ -5254,18 +5311,19 @@ AND Type = @InternalPersonType)");
var now = DateTime.UtcNow;
var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+ ("Type=" + itemValueTypes[0]) :
+ ("Type in (" + string.Join(',', itemValueTypes) + ")");
InternalItemsQuery typeSubQuery = null;
- Dictionary<string, string> itemCountColumns = null;
+ string itemCountColumns = null;
+ var stringBuilder = new StringBuilder(1024);
var typesToCount = query.IncludeItemTypes;
if (typesToCount.Length > 0)
{
- var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B");
+ stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B");
typeSubQuery = new InternalItemsQuery(query.User)
{
@@ -5281,20 +5339,22 @@ AND Type = @InternalPersonType)");
};
var whereClauses = GetWhereClauses(typeSubQuery, null);
- whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")");
+ stringBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses)
+ .Append(" AND ")
+ .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ")
+ .Append(typeClause)
+ .Append(")) as itemTypes");
- itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses);
-
- itemCountColumns = new Dictionary<string, string>()
- {
- { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
- };
+ itemCountColumns = stringBuilder.ToString();
+ stringBuilder.Clear();
}
List<string> columns = _retriveItemColumns.ToList();
- if (itemCountColumns != null)
+ // Unfortunately we need to add it to columns to ensure the order of the columns in the select
+ if (!string.IsNullOrEmpty(itemCountColumns))
{
- columns.AddRange(itemCountColumns.Values);
+ columns.Add(itemCountColumns);
}
// do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo
@@ -5315,20 +5375,20 @@ AND Type = @InternalPersonType)");
IsSeries = query.IsSeries
};
- columns = GetFinalColumnsToSelect(query, columns);
-
- var commandText = "select "
- + string.Join(',', columns)
- + GetFromText()
- + GetJoinUserDataText(query);
+ SetFinalColumnsToSelect(query, columns);
var innerWhereClauses = GetWhereClauses(innerQuery, null);
- var innerWhereText = innerWhereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", innerWhereClauses);
+ stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ")
+ .Append(typeClause)
+ .Append(" AND ItemId in (select guid from TypedBaseItems");
+ if (innerWhereClauses.Count > 0)
+ {
+ stringBuilder.Append(" where ")
+ .AppendJoin(" AND ", innerWhereClauses);
+ }
- var whereText = " where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))";
+ stringBuilder.Append("))");
var outerQuery = new InternalItemsQuery(query.User)
{
@@ -5353,23 +5413,31 @@ AND Type = @InternalPersonType)");
};
var outerWhereClauses = GetWhereClauses(outerQuery, null);
-
if (outerWhereClauses.Count != 0)
{
- whereText += " AND " + string.Join(" AND ", outerWhereClauses);
+ stringBuilder.Append(" AND ")
+ .AppendJoin(" AND ", outerWhereClauses);
}
- commandText += whereText + " group by PresentationUniqueKey";
+ var whereText = stringBuilder.ToString();
+ stringBuilder.Clear();
+
+ stringBuilder.Append("select ")
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query))
+ .Append(whereText)
+ .Append(" group by PresentationUniqueKey");
if (query.OrderBy.Count != 0
|| query.SimilarTo != null
|| !string.IsNullOrEmpty(query.SearchTerm))
{
- commandText += GetOrderByText(query);
+ stringBuilder.Append(GetOrderByText(query));
}
else
{
- commandText += " order by SortName";
+ stringBuilder.Append(" order by SortName");
}
if (query.Limit.HasValue || query.StartIndex.HasValue)
@@ -5378,32 +5446,39 @@ AND Type = @InternalPersonType)");
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ stringBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ stringBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
- var statementTexts = new List<string>();
+ string commandText = string.Empty;
+
if (!isReturningZeroItems)
{
- statementTexts.Add(commandText);
+ commandText = stringBuilder.ToString();
}
+ string countText = string.Empty;
if (query.EnableTotalRecordCount)
{
- var countText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
+ stringBuilder.Clear();
+ var columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
+ SetFinalColumnsToSelect(query, columnsToSelect);
+ stringBuilder.Append("select ")
+ .AppendJoin(',', columnsToSelect)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query))
+ .Append(whereText);
- statementTexts.Add(countText);
+ countText = stringBuilder.ToString();
}
var list = new List<(BaseItem, ItemCounts)>();
@@ -5413,11 +5488,9 @@ AND Type = @InternalPersonType)");
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts);
-
if (!isReturningZeroItems)
{
- using (var statement = statements[0])
+ using (var statement = PrepareStatement(db, commandText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5458,13 +5531,7 @@ AND Type = @InternalPersonType)");
if (query.EnableTotalRecordCount)
{
- commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
-
- using (var statement = statements[statements.Length - 1])
+ using (var statement = PrepareStatement(db, countText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5501,7 +5568,7 @@ AND Type = @InternalPersonType)");
return result;
}
- private static ItemCounts GetItemCounts(IReadOnlyList<IResultSetValue> reader, int countStartColumn, string[] typesToCount)
+ private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, string[] typesToCount)
{
var counts = new ItemCounts();
@@ -5732,7 +5799,7 @@ AND Type = @InternalPersonType)");
}
}
- private PersonInfo GetPerson(IReadOnlyList<IResultSetValue> reader)
+ private PersonInfo GetPerson(IReadOnlyList<ResultSetValue> reader)
{
var item = new PersonInfo
{
@@ -5939,7 +6006,7 @@ AND Type = @InternalPersonType)");
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>ChapterInfo.</returns>
- private MediaStream GetMediaStream(IReadOnlyList<IResultSetValue> reader)
+ private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
{
var item = new MediaStream
{
@@ -6240,7 +6307,7 @@ AND Type = @InternalPersonType)");
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>MediaAttachment.</returns>
- private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader)
+ private MediaAttachment GetMediaAttachment(IReadOnlyList<ResultSetValue> reader)
{
var item = new MediaAttachment
{
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index e0ebd0a6c..ef9af1dcd 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -348,7 +350,7 @@ namespace Emby.Server.Implementations.Data
/// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader"></param>
- private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
+ private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
{
var userData = new UserItemData();
diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs
index 7044b1d19..064664e1f 100644
--- a/Emby.Server.Implementations/Data/TypeMapper.cs
+++ b/Emby.Server.Implementations/Data/TypeMapper.cs
@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data
/// This holds all the types in the running assemblies
/// so that we can de-serialize properly when we don't have strong types.
/// </summary>
- private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
+ private readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
/// <summary>
/// Gets the type.
@@ -21,26 +21,16 @@ namespace Emby.Server.Implementations.Data
/// <param name="typeName">Name of the type.</param>
/// <returns>Type.</returns>
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
- public Type GetType(string typeName)
+ public Type? GetType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
throw new ArgumentNullException(nameof(typeName));
}
- return _typeMap.GetOrAdd(typeName, LookupType);
- }
-
- /// <summary>
- /// Lookups the type.
- /// </summary>
- /// <param name="typeName">Name of the type.</param>
- /// <returns>Type.</returns>
- private Type LookupType(string typeName)
- {
- return AppDomain.CurrentDomain.GetAssemblies()
- .Select(a => a.GetType(typeName))
- .FirstOrDefault(t => t != null);
+ return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
+ .Select(a => a.GetType(k))
+ .FirstOrDefault(t => t != null));
}
}
}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index fa6ac95fd..3d15b3e76 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 4ae35039a..7411239a1 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 14f6f565c..9c90de1ed 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -9,6 +9,7 @@
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
+ <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
@@ -27,11 +28,11 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
- <PackageReference Include="sharpcompress" Version="0.28.2" />
- <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.2.0" />
+ <PackageReference Include="sharpcompress" Version="0.28.3" />
+ <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
@@ -44,6 +45,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
<AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 14201ead2..0a4efd73c 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.StartDiscovery();
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
-
- _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
}
private void Stop()
@@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
_timer?.Dispose();
-
- _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
- }
-
- private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
- {
- NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
}
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index ae1b51b4c..5bb4100ba 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index 824bb85f4..e0ca02d98 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 3624e079f..2e72b18f5 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Net.Sockets;
using System.Threading;
@@ -56,8 +54,8 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- _udpServer = new UdpServer(_logger, _appHost, _config);
- _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+ _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
+ _udpServer.Start(_cancellationTokenSource.Token);
}
catch (SocketException ex)
{
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 1989e9ed2..d3bcd5e13 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
private readonly object _syncLock = new object();
- private Timer _updateTimer;
+ private Timer? _updateTimer;
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
{
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
- void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
+ private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
{
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
{
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
}
- if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
+ if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
{
keys = new List<BaseItem>();
_changedItems[e.UserId] = keys;
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- private void UpdateTimerCallback(object state)
+ private void UpdateTimerCallback(object? state)
{
lock (_syncLock)
{
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index cd1b9cba0..a7647caf9 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -43,14 +43,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetSession((HttpContext)requestContext);
}
- public async Task<User> GetUser(HttpContext requestContext)
+ public async Task<User?> GetUser(HttpContext requestContext)
{
var session = await GetSession(requestContext).ConfigureAwait(false);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
- public Task<User> GetUser(object requestContext)
+ public Task<User?> GetUser(object requestContext)
{
return GetUser(((HttpRequest)requestContext).HttpContext);
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 06acb5606..8f7d60669 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Buffers;
using System.IO.Pipelines;
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index b71ffdaee..f86bfd755 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 7435e9d0b..47a83d77c 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 3353fae9d..aa80bccd7 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 27096ed33..64d802457 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
@@ -61,7 +62,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">filename</exception>
- public virtual string ResolveShortcut(string filename)
+ public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
{
@@ -243,8 +244,8 @@ namespace Emby.Server.Implementations.IO
{
result.Length = fileInfo.Length;
- // Issue #2354 get the size of files behind symbolic links
- if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+ // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
+ if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{
try
{
@@ -601,7 +602,7 @@ namespace Emby.Server.Implementations.IO
return GetFiles(path, null, false, recursive);
}
- public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+ public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -618,13 +619,13 @@ namespace Emby.Server.Implementations.IO
{
files = files.Where(i =>
{
- var ext = i.Extension;
- if (ext == null)
+ var ext = i.Extension.AsSpan();
+ if (ext.IsEmpty)
{
return false;
}
- return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+ return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
@@ -636,8 +637,7 @@ namespace Emby.Server.Implementations.IO
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
- return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
- .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
+ return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions));
}
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -655,7 +655,7 @@ namespace Emby.Server.Implementations.IO
return GetFilePaths(path, null, false, recursive);
}
- public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+ public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -672,13 +672,13 @@ namespace Emby.Server.Implementations.IO
{
files = files.Where(i =>
{
- var ext = Path.GetExtension(i);
- if (ext == null)
+ var ext = Path.GetExtension(i.AsSpan());
+ if (ext.IsEmpty)
{
return false;
}
- return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+ return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
index e6696b8c4..76c58d5dc 100644
--- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
+++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO
public string Extension => ".mblink";
- public string Resolve(string shortcutPath)
+ public string? Resolve(string shortcutPath)
{
if (string.IsNullOrEmpty(shortcutPath))
{
diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs
index c16ebd61b..e4f5f4cf0 100644
--- a/Emby.Server.Implementations/IO/StreamHelper.cs
+++ b/Emby.Server.Implementations/IO/StreamHelper.cs
@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.IO
{
public class StreamHelper : IStreamHelper
{
- public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
+ public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index f719dc5f8..a430b9e72 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#nullable enable
namespace Emby.Server.Implementations
{
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 6fa3c1c61..833fb0b7a 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index 161b4c452..ff5f26ce0 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 50c531482..900b3fd9c 100644
--- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 0224ab32a..859017f86 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 381788231..6da431c68 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
index a4c106e87..b8f0f0d65 100644
--- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 3380e29d4..c7d113963 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
- if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
+ if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 236453e80..6c65b5899 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index e30a67593..5384c04b3 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Linq;
using DotNet.Globbing;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 4d207471a..028673529 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -694,25 +696,32 @@ namespace Emby.Server.Implementations.Library
}
private IEnumerable<BaseItem> ResolveFileList(
- IEnumerable<FileSystemMetadata> fileList,
+ IReadOnlyList<FileSystemMetadata> fileList,
IDirectoryService directoryService,
Folder parent,
string collectionType,
IItemResolver[] resolvers,
LibraryOptions libraryOptions)
{
- return fileList.Select(f =>
+ // Given that fileList is a list we can save enumerator allocations by indexing
+ for (var i = 0; i < fileList.Count; i++)
{
+ var file = fileList[i];
+ BaseItem result = null;
try
{
- return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
+ result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error resolving path {path}", f.FullName);
- return null;
+ _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
+ }
+
+ if (result != null)
+ {
+ yield return result;
}
- }).Where(i => i != null);
+ }
}
/// <summary>
@@ -1063,17 +1072,17 @@ namespace Emby.Server.Implementations.Library
// Start by just validating the children of the root, but go no further
await RootFolder.ValidateChildren(
new SimpleProgress<double>(),
- cancellationToken,
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
- recursive: false).ConfigureAwait(false);
+ recursive: false,
+ cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().ValidateChildren(
new SimpleProgress<double>(),
- cancellationToken,
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
- recursive: false).ConfigureAwait(false);
+ recursive: false,
+ cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
@@ -1093,7 +1102,7 @@ namespace Emby.Server.Implementations.Library
innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
// Validate the entire media library
- await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
+ await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
progress.Report(96);
@@ -2074,7 +2083,7 @@ namespace Emby.Server.Implementations.Library
return new List<Folder>();
}
- return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList());
+ return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>());
}
public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren)
@@ -2099,10 +2108,10 @@ namespace Emby.Server.Implementations.Library
return GetCollectionFoldersInternal(item, allUserRootChildren);
}
- private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren)
+ private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
{
return allUserRootChildren
- .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase))
+ .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
@@ -2110,9 +2119,9 @@ namespace Emby.Server.Implementations.Library
{
if (!(item is CollectionFolder collectionFolder))
{
+ // List.Find is more performant than FirstOrDefault due to enumerator allocation
collectionFolder = GetCollectionFolders(item)
- .OfType<CollectionFolder>()
- .FirstOrDefault();
+ .Find(folder => folder is CollectionFolder) as CollectionFolder;
}
return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
@@ -2498,8 +2507,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public bool IsVideoFile(string path)
{
- var resolver = new VideoResolver(GetNamingOptions());
- return resolver.IsVideoFile(path);
+ return VideoResolver.IsVideoFile(path, GetNamingOptions());
}
/// <inheritdoc />
@@ -2677,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
return changed;
}
+ /// <inheritdoc />
public NamingOptions GetNamingOptions()
{
if (_namingOptions == null)
@@ -2690,13 +2699,12 @@ namespace Emby.Server.Implementations.Library
public ItemLookupInfo ParseName(string name)
{
- var resolver = new VideoResolver(GetNamingOptions());
-
- var result = resolver.CleanDateTime(name);
+ var namingOptions = GetNamingOptions();
+ var result = VideoResolver.CleanDateTime(name, namingOptions);
return new ItemLookupInfo
{
- Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
+ Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name,
Year = result.Year
};
}
@@ -2710,9 +2718,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
- var videoListResolver = new VideoListResolver(namingOptions);
-
- var videos = videoListResolver.Resolve(fileSystemChildren);
+ var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
@@ -2756,9 +2762,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
- var videoListResolver = new VideoListResolver(namingOptions);
-
- var videos = videoListResolver.Resolve(fileSystemChildren);
+ var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index c2951dd15..4ef7923db 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 85d6d3043..b812b6b61 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -350,7 +352,7 @@ namespace Emby.Server.Implementations.Library
private string[] NormalizeLanguage(string language)
{
- if (language == null)
+ if (string.IsNullOrEmpty(language))
{
return Array.Empty<string>();
}
@@ -379,8 +381,7 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
- ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
+ var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex == null
@@ -409,9 +410,7 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
- ? Array.Empty<string>()
- : NormalizeLanguage(user.AudioLanguagePreference);
+ var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 28fa06239..b833122ea 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index f8bae4fd1..06300adeb 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 0de4edb7e..86b8039fa 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Diagnostics.CodeAnalysis;
using MediaBrowser.Common.Providers;
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index 1d9b44874..ac75e5d3a 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.IO;
using System.Linq;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 4ad84579d..e893d6335 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index bf32381eb..8e1eccb10 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 60f82806f..3d2ae95d2 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Linq;
using System.Threading.Tasks;
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 16050185f..cdb492022 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -45,11 +47,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+ var namingOptions = LibraryManager.GetNamingOptions();
// If the path is a file check for a matching extensions
- var parser = new VideoResolver(namingOptions);
-
if (args.IsDirectory)
{
TVideoType video = null;
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else if (IsDvdFile(filename))
{
- videoInfo = parser.ResolveDirectory(args.Path);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else
{
- var videoInfo = parser.Resolve(args.Path, false, false);
+ var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
if (videoInfo == null)
{
@@ -250,10 +250,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void Set3DFormat(Video video)
{
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
-
- var resolver = new Format3DParser(namingOptions);
- var result = resolver.Parse(video.Path);
+ var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
Set3DFormat(video, result.Is3D, result.Format3D);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 0525c7e30..68076730b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
index 7dbce7a6e..7aaee017d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
index 92fb2a753..fa45ccf84 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 295e9e120..69d71d0d9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.IO;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 16bf4dc4a..97f96f746 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -1,9 +1,12 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -255,10 +258,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+ var namingOptions = LibraryManager.GetNamingOptions();
- var resolver = new VideoListResolver(namingOptions);
- var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList();
+ var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
var result = new MultiItemResolverResult
{
@@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return returnVideo;
}
- private bool IsInvalid(Folder parent, string collectionType)
+ private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
{
if (parent != null)
{
@@ -545,12 +547,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- if (string.IsNullOrEmpty(collectionType))
+ if (collectionType.IsEmpty)
{
return false;
}
- return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
+ return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
index 204c8a62e..534bc80dd 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index 3cb6542cf..57bf40e9e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 5f051321f..ecd44be47 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
index 99f304190..7b4e14334 100644
--- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 6f29bc649..d6ae91056 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Linq;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 768e2e4f5..7d707df18 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Globalization;
using Emby.Naming.TV;
using MediaBrowser.Controller.Entities.TV;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index 8fc3e3e75..a1562abd3 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
index 62268fce9..9599faea4 100644
--- a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index bcdf854ca..26e615fa0 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 827e3c64b..8aa605a90 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -220,7 +222,7 @@ namespace Emby.Server.Implementations.Library
var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration
- if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
+ if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
{
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
@@ -239,7 +241,7 @@ namespace Emby.Server.Implementations.Library
{
// Enforce MinResumeDuration
var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
- if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book))
+ if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
{
positionTicks = 0;
data.Played = playedToCompletion = true;
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index ac041bcf6..e2da672a3 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 7a6b1d8b6..3fcadf5b1 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 28a2095e1..797063120 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 9372b0f6c..26e4ef1ed 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
index 8c27ca76e..0ec52a959 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
@@ -6,7 +6,6 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
-
internal class EpgChannelData
{
@@ -39,13 +38,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public ChannelInfo GetChannelById(string id)
+ public ChannelInfo? GetChannelById(string id)
=> _channelsById.GetValueOrDefault(id);
- public ChannelInfo GetChannelByNumber(string number)
+ public ChannelInfo? GetChannelByNumber(string number)
=> _channelsByNumber.GetValueOrDefault(number);
- public ChannelInfo GetChannelByName(string name)
+ public ChannelInfo? GetChannelByName(string name)
=> _channelsByName.GetValueOrDefault(name);
public static string NormalizeName(string value)
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index 1cac9cb96..bdab8c3e4 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 1efa90e25..6c52a9a73 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 5b6f8cc49..cbdc7c707 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 6824aa442..ebad4eddf 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
index ba916af38..098f193fb 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
@@ -1,21 +1,23 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.LiveTv;
namespace Emby.Server.Implementations.LiveTv
{
+ /// <summary>
+ /// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />.
+ /// </summary>
public class LiveTvConfigurationFactory : IConfigurationFactory
{
+ /// <inheritdoc />
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
- ConfigurationType = typeof(LiveTvOptions),
- Key = "livetv"
+ ConfigurationType = typeof(LiveTvOptions),
+ Key = "livetv"
}
};
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 6af49dd45..21e1409ac 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 1145d8aa1..d964769b5 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -2264,7 +2266,7 @@ namespace Emby.Server.Implementations.LiveTv
if (dataSourceChanged)
{
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
return info;
@@ -2307,7 +2309,7 @@ namespace Emby.Server.Implementations.LiveTv
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return info;
}
@@ -2319,7 +2321,7 @@ namespace Emby.Server.Implementations.LiveTv
config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
@@ -2353,7 +2355,7 @@ namespace Emby.Server.Implementations.LiveTv
var tunerChannelMappings =
tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index 3a738fd5d..ecd28097d 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
index 582b64923..15df0dcf1 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.LiveTv;
@@ -10,34 +9,55 @@ using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.LiveTv
{
- public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
+ /// <summary>
+ /// The "Refresh Guide" scheduled task.
+ /// </summary>
+ public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILiveTvManager _liveTvManager;
private readonly IConfigurationManager _config;
- public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class.
+ /// </summary>
+ /// <param name="liveTvManager">The live tv manager.</param>
+ /// <param name="config">The configuration manager.</param>
+ public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
{
_liveTvManager = liveTvManager;
_config = config;
}
+ /// <inheritdoc />
public string Name => "Refresh Guide";
+ /// <inheritdoc />
public string Description => "Downloads channel information from live tv services.";
+ /// <inheritdoc />
public string Category => "Live TV";
- public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <inheritdoc />
+ public string Key => "RefreshGuide";
+
+ /// <inheritdoc />
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
var manager = (LiveTvManager)_liveTvManager;
return manager.RefreshChannels(progress, cancellationToken);
}
- /// <summary>
- /// Creates the triggers that define when the task will run.
- /// </summary>
- /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ /// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
@@ -51,13 +71,5 @@ namespace Emby.Server.Implementations.LiveTv
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
-
- public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
-
- public string Key => "RefreshGuide";
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index fbcd4ef37..5941613cf 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -38,6 +40,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public virtual bool IsSupported => true;
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+
public abstract string Type { get; }
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
index 740cbb66e..0f0453189 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
internal class Channels
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
index 09d77f838..42068cd34 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index bbac6e055..54de841fe 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -581,7 +583,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
- _networkManager,
_streamHelper);
}
@@ -622,7 +623,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
- _networkManager,
_streamHelper);
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index a7fda1d72..3016eeda2 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index b16ccc561..58e0c7448 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,7 +12,6 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -28,7 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IServerApplicationHost _appHost;
private readonly IHdHomerunChannelCommands _channelCommands;
private readonly int _numTuners;
- private readonly INetworkManager _networkManager;
public HdHomerunUdpStream(
MediaSourceInfo mediaSource,
@@ -40,12 +40,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
ILogger logger,
IConfigurationManager configurationManager,
IServerApplicationHost appHost,
- INetworkManager networkManager,
IStreamHelper streamHelper)
: base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
{
_appHost = appHost;
- _networkManager = networkManager;
OriginalStreamId = originalStreamId;
_channelCommands = channelCommands;
_numTuners = numTuners;
@@ -126,7 +124,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using (udpClient)
using (hdHomerunManager)
{
- if (!(ex is OperationCanceledException))
+ if (ex is not OperationCanceledException)
{
Logger.LogError(ex, "Error opening live stream:");
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index f8baf55da..96a678c1d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 4b170b2e4..8fa6f5ad6 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -27,6 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
+ private static readonly string[] _disallowedSharedStreamExtensions =
+ {
+ ".mkv",
+ ".mp4",
+ ".m3u8",
+ ".mpd"
+ };
+
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
@@ -65,7 +75,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
- return await new M3uParser(Logger, _httpClientFactory, _appHost)
+ return await new M3uParser(Logger, _httpClientFactory)
.Parse(info, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
}
@@ -86,14 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- private static readonly string[] _disallowedSharedStreamExtensions =
- {
- ".mkv",
- ".mp4",
- ".m3u8",
- ".mpd"
- };
-
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = info.TunerCount;
@@ -128,7 +130,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
- using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
+ using (var stream = await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 84d416149..40a162890 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -19,15 +21,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3uParser
{
+ private const string ExtInfPrefix = "#EXTINF:";
+
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerApplicationHost _appHost;
- public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
+ public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
}
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
@@ -59,8 +61,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return File.OpenRead(info.Url);
}
- private const string ExtInfPrefix = "#EXTINF:";
-
private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
{
var channels = new List<ChannelInfo>();
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index eeb2426f4..f572151b8 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -89,8 +91,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var taskCompletionSource = new TaskCompletionSource<bool>();
- var now = DateTime.UtcNow;
-
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
// OpenedMediaSource.Protocol = MediaProtocol.File;
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!taskCompletionSource.Task.Result)
{
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
- throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
+ throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 9f84d3a3c..3d6e159b1 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -116,5 +116,7 @@
"TaskCleanLogs": "حذف دليل السجل",
"TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الموضوع.",
"TaskCleanActivityLog": "حذف سجل الأنشطة",
- "Default": "الإعدادات الافتراضية"
+ "Default": "الإعدادات الافتراضية",
+ "Undefined": "غير معرف",
+ "Forced": "ملحقة"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index f8f595faa..65964f6d9 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -117,5 +117,7 @@
"TaskRefreshChannels": "Refresh Channels",
"TaskRefreshChannelsDescription": "Refreshes internet channel information.",
"TaskDownloadMissingSubtitles": "Download missing subtitles",
- "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration."
+ "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
+ "TaskOptimizeDatabase": "Optimize database",
+ "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance."
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 16fde325f..91939843f 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpiar registro de actividad",
"Undefined": "Indefinido",
"Forced": "Forzado",
- "Default": "Predeterminado"
+ "Default": "Predeterminado",
+ "TaskOptimizeDatabase": "Optimizar la base de datos",
+ "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index f5683f35b..32d4f3a8b 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -29,9 +29,9 @@
"Collections": "Samlingar",
"ChapterNameValue": "Kapittel {0}",
"Channels": "Kanalar",
- "CameraImageUploadedFrom": "Eit nytt kamera bilete har blitt lasta opp frå {0}",
+ "CameraImageUploadedFrom": "Eit nytt kamerabilete har blitt lasta opp frå {0}",
"Books": "Bøker",
- "AuthenticationSucceededWithUserName": "{0} Har logga inn",
+ "AuthenticationSucceededWithUserName": "{0} har logga inn",
"Artists": "Artistar",
"Application": "Program",
"AppDeviceValues": "App: {0}, Eining: {1}",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 220e423bf..b1ff28c2c 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -1,9 +1,10 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
@@ -167,12 +168,22 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public CultureDto FindLanguageInfo(string language)
- => GetCultures()
- .FirstOrDefault(i =>
- string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase)
- || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase)
- || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase)
- || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
+ {
+ // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
+ for (var i = 0; i < _cultures.Count; i++)
+ {
+ var culture = _cultures[i];
+ if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
+ || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
+ || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
+ || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
+ {
+ return culture;
+ }
+ }
+
+ return default;
+ }
/// <inheritdoc />
public IEnumerable<CountryInfo> GetCountries()
@@ -222,7 +233,7 @@ namespace Emby.Server.Implementations.Localization
throw new ArgumentNullException(nameof(rating));
}
- if (_unratedValues.Contains(rating, StringComparer.OrdinalIgnoreCase))
+ if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -250,11 +261,11 @@ namespace Emby.Server.Implementations.Localization
var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1)
{
- rating = rating.Substring(index).TrimStart(':').Trim();
+ var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim();
- if (!string.IsNullOrWhiteSpace(rating))
+ if (!trimmedRating.IsEmpty)
{
- return GetRatingLevel(rating);
+ return GetRatingLevel(trimmedRating.ToString());
}
}
@@ -316,7 +327,8 @@ namespace Emby.Server.Implementations.Localization
return _dictionaries.GetOrAdd(
culture,
- f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
+ (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
+ this);
}
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 031b5d2e7..8aaa1f7bb 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 0781a0e33..137728616 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index 4e25768cf..a8b18d292 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 2d1a559f1..9a1ca9946 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 14df20936..8fd61f2bc 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -396,7 +394,7 @@ namespace Emby.Server.Implementations.Plugins
Category = packageInfo.Category,
Changelog = versionInfo.Changelog ?? string.Empty,
Description = packageInfo.Description,
- Id = new Guid(packageInfo.Id),
+ Id = packageInfo.Id,
Name = packageInfo.Name,
Overview = packageInfo.Overview,
Owner = packageInfo.Owner,
@@ -457,7 +455,8 @@ namespace Emby.Server.Implementations.Plugins
try
{
_logger.LogDebug("Creating instance of {Type}", type);
- var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
+ // _appHost.ServiceProvider is already assigned when we create the plugins
+ var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider!, type);
if (plugin == null)
{
// Create a dummy record for the providers.
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index c627bc3fc..b32c70eb9 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -127,6 +129,7 @@ namespace Emby.Server.Implementations.QuickConnect
return result;
}
+ /// <inheritdoc/>
public void AuthenticateRequest(AuthenticationRequest request, string token)
{
if (!_quickConnectTokens.TryGetValue(token, out var entry))
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 101d9b537..d7e320754 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -709,11 +711,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
}
- return new DailyTrigger
- {
- TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
- TaskOptions = options
- };
+ return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
}
if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
@@ -728,12 +726,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));
}
- return new WeeklyTrigger
- {
- TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
- DayOfWeek = info.DayOfWeek.Value,
- TaskOptions = options
- };
+ return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
}
if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
@@ -743,16 +736,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));
}
- return new IntervalTrigger
- {
- Interval = TimeSpan.FromTicks(info.IntervalTicks.Value),
- TaskOptions = options
- };
+ return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
}
if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
{
- return new StartupTrigger();
+ return new StartupTrigger(options);
}
throw new ArgumentException("Unrecognized trigger type: " + info.Type);
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index af316e108..4f0df75bf 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 2312c85d9..baeb86a22 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -140,8 +140,10 @@ namespace Emby.Server.Implementations.ScheduledTasks
previouslyFailedImages.Add(key);
var parentPath = Path.GetDirectoryName(failHistoryPath);
-
- Directory.CreateDirectory(parentPath);
+ if (parentPath != null)
+ {
+ Directory.CreateDirectory(parentPath);
+ }
string text = string.Join('|', previouslyFailedImages);
File.WriteAllText(failHistoryPath, text);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
index 4abbf784b..50ba9bc89 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -75,4 +75,4 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return Enumerable.Empty<TaskTriggerInfo>();
}
}
-} \ No newline at end of file
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
new file mode 100644
index 000000000..1ad1d0f50
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Server.Implementations;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Optimizes Jellyfin's database by issuing a VACUUM command.
+ /// </summary>
+ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ private readonly ILogger<OptimizeDatabaseTask> _logger;
+ private readonly ILocalizationManager _localization;
+ private readonly JellyfinDbProvider _provider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
+ /// </summary>
+ public OptimizeDatabaseTask(
+ ILogger<OptimizeDatabaseTask> logger,
+ ILocalizationManager localization,
+ JellyfinDbProvider provider)
+ {
+ _logger = logger;
+ _localization = localization;
+ _provider = provider;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public string Key => "OptimizeDatabaseTask";
+
+ /// <inheritdoc />
+ public bool IsHidden => false;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[]
+ {
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ _logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
+
+ try
+ {
+ using var context = _provider.CreateContext();
+ if (context.Database.IsSqlite())
+ {
+ context.Database.ExecuteSqlRaw("PRAGMA optimize");
+ context.Database.ExecuteSqlRaw("VACUUM");
+ _logger.LogInformation("jellyfin.db optimized successfully!");
+ }
+ else
+ {
+ _logger.LogInformation("This database doesn't support optimization");
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error while optimizing jellyfin.db");
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
index 3b40320ab..29ab6a73d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
@@ -8,29 +8,31 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that fires everyday.
/// </summary>
- public class DailyTrigger : ITaskTrigger
+ public sealed class DailyTrigger : ITaskTrigger
{
- /// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
+ private readonly TimeSpan _timeOfDay;
+ private Timer? _timer;
/// <summary>
- /// Gets or sets the time of day to trigger the task to run.
+ /// Initializes a new instance of the <see cref="DailyTrigger"/> class.
/// </summary>
- /// <value>The time of day.</value>
- public TimeSpan TimeOfDay { get; set; }
+ /// <param name="timeofDay">The time of day to trigger the task to run.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public DailyTrigger(TimeSpan timeofDay, TaskOptions taskOptions)
+ {
+ _timeOfDay = timeofDay;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -45,14 +47,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
var now = DateTime.Now;
- var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date;
- triggerDate = triggerDate.Add(TimeOfDay);
+ var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date;
+ triggerDate = triggerDate.Add(_timeOfDay);
var dueTime = triggerDate - now;
logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime);
- Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -68,10 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
index b04fd7c7e..30568e809 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
@@ -9,31 +9,32 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that runs repeatedly on an interval.
/// </summary>
- public class IntervalTrigger : ITaskTrigger
+ public sealed class IntervalTrigger : ITaskTrigger
{
+ private readonly TimeSpan _interval;
private DateTime _lastStartDate;
+ private Timer? _timer;
/// <summary>
- /// Occurs when [triggered].
+ /// Initializes a new instance of the <see cref="IntervalTrigger"/> class.
/// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
- /// Gets or sets the interval.
- /// </summary>
- /// <value>The interval.</value>
- public TimeSpan Interval { get; set; }
+ /// <param name="interval">The interval.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions)
+ {
+ _interval = interval;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -55,7 +56,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
else
{
- triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(Interval);
+ triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(_interval);
}
if (DateTime.UtcNow > triggerDate)
@@ -71,7 +72,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
dueTime = maxDueTime;
}
- Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -87,10 +88,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
index 7cd5493da..18b9a8b75 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
@@ -10,24 +10,28 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Class StartupTaskTrigger.
/// </summary>
- public class StartupTrigger : ITaskTrigger
+ public sealed class StartupTrigger : ITaskTrigger
{
+ public const int DelayMs = 3000;
+
/// <summary>
- /// Occurs when [triggered].
+ /// Initializes a new instance of the <see cref="StartupTrigger"/> class.
/// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- public int DelayMs { get; set; }
+ /// <param name="taskOptions">The options of this task.</param>
+ public StartupTrigger(TaskOptions taskOptions)
+ {
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
- public StartupTrigger()
- {
- DelayMs = 3000;
- }
+ /// <summary>
+ /// Gets the options of this task.
+ /// </summary>
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
index 0c0ebec08..36ae190b0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
@@ -8,35 +8,34 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that fires on a weekly basis.
/// </summary>
- public class WeeklyTrigger : ITaskTrigger
+ public sealed class WeeklyTrigger : ITaskTrigger
{
- /// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
- /// Gets or sets the time of day to trigger the task to run.
- /// </summary>
- /// <value>The time of day.</value>
- public TimeSpan TimeOfDay { get; set; }
+ private readonly TimeSpan _timeOfDay;
+ private readonly DayOfWeek _dayOfWeek;
+ private Timer? _timer;
/// <summary>
- /// Gets or sets the day of week.
+ /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class.
/// </summary>
- /// <value>The day of week.</value>
- public DayOfWeek DayOfWeek { get; set; }
+ /// <param name="timeofDay">The time of day to trigger the task to run.</param>
+ /// <param name="dayOfWeek">The day of week.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public WeeklyTrigger(TimeSpan timeofDay, DayOfWeek dayOfWeek, TaskOptions taskOptions)
+ {
+ _timeOfDay = timeofDay;
+ _dayOfWeek = dayOfWeek;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -51,7 +50,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
var triggerDate = GetNextTriggerDateTime();
- Timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -63,22 +62,22 @@ namespace Emby.Server.Implementations.ScheduledTasks
var now = DateTime.Now;
// If it's on the same day
- if (now.DayOfWeek == DayOfWeek)
+ if (now.DayOfWeek == _dayOfWeek)
{
// It's either later today, or a week from now
- return now.TimeOfDay < TimeOfDay ? now.Date.Add(TimeOfDay) : now.Date.AddDays(7).Add(TimeOfDay);
+ return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay);
}
var triggerDate = now.Date;
// Walk the date forward until we get to the trigger day
- while (triggerDate.DayOfWeek != DayOfWeek)
+ while (triggerDate.DayOfWeek != _dayOfWeek)
{
triggerDate = triggerDate.AddDays(1);
}
// Return the trigger date plus the time offset
- return triggerDate.Add(TimeOfDay);
+ return triggerDate.Add(_timeOfDay);
}
/// <summary>
@@ -94,10 +93,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 27024e4e1..5ff73de81 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -19,7 +19,10 @@ namespace Emby.Server.Implementations.Serialization
new ConcurrentDictionary<string, XmlSerializer>();
private static XmlSerializer GetSerializer(Type type)
- => _serializers.GetOrAdd(type.FullName, _ => new XmlSerializer(type));
+ => _serializers.GetOrAdd(
+ type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
+ (_, t) => new XmlSerializer(t),
+ type);
/// <summary>
/// Serializes to writer.
@@ -38,7 +41,7 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="type">The type.</param>
/// <param name="stream">The stream.</param>
/// <returns>System.Object.</returns>
- public object DeserializeFromStream(Type type, Stream stream)
+ public object? DeserializeFromStream(Type type, Stream stream)
{
using (var reader = XmlReader.Create(stream))
{
@@ -81,7 +84,7 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="type">The type.</param>
/// <param name="file">The file.</param>
/// <returns>System.Object.</returns>
- public object DeserializeFromFile(Type type, string file)
+ public object? DeserializeFromFile(Type type, string file)
{
using (var stream = File.OpenRead(file))
{
@@ -95,7 +98,7 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="type">The type.</param>
/// <param name="buffer">The buffer.</param>
/// <returns>System.Object.</returns>
- public object DeserializeFromBytes(Type type, byte[] buffer)
+ public object? DeserializeFromBytes(Type type, byte[] buffer)
{
using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true))
{
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index ac589b03c..6cf9a8f71 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -25,6 +25,10 @@ namespace Emby.Server.Implementations
cacheDirectoryPath,
webDirectoryPath)
{
+ // ProgramDataPath cannot change when the server is running, so cache these to avoid allocations.
+ RootFolderPath = Path.Join(ProgramDataPath, "root");
+ DefaultUserViewsPath = Path.Combine(RootFolderPath, "default");
+ DefaultInternalMetadataPath = Path.Combine(ProgramDataPath, "metadata");
InternalMetadataPath = DefaultInternalMetadataPath;
}
@@ -32,13 +36,13 @@ namespace Emby.Server.Implementations
/// Gets the path to the base root media directory.
/// </summary>
/// <value>The root folder path.</value>
- public string RootFolderPath => Path.Combine(ProgramDataPath, "root");
+ public string RootFolderPath { get; }
/// <summary>
/// Gets the path to the default user view directory. Used if no specific user view is defined.
/// </summary>
/// <value>The default user views path.</value>
- public string DefaultUserViewsPath => Path.Combine(RootFolderPath, "default");
+ public string DefaultUserViewsPath { get; }
/// <summary>
/// Gets the path to the People directory.
@@ -98,7 +102,7 @@ namespace Emby.Server.Implementations
public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users");
/// <inheritdoc/>
- public string DefaultInternalMetadataPath => Path.Combine(ProgramDataPath, "metadata");
+ public string DefaultInternalMetadataPath { get; }
/// <inheritdoc />
public string InternalMetadataPath { get; set; }
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index b8c778690..544c8e5fd 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 54e64cfeb..2a14a8c7b 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index a653b58c2..9fa92a53a 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,6 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1600
-#nullable enable
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 60698e803..2b0ab536f 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 7657cc74e..42e644970 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
var audio = x as IHasAlbumArtist;
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 7dfdd9ecf..1db3f5e9c 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
var audio = x as Audio;
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
index 756d3c5b6..98bee3fd9 100644
--- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
public string Name => ItemSortBy.Artist;
/// <inheritdoc />
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
- private static string GetValue(BaseItem x)
+ private static string? GetValue(BaseItem? x)
{
if (!(x is Audio audio))
{
diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
index 980954ba0..5f142fa4b 100644
--- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index fa136c36d..d20dedc2d 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -15,14 +15,14 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
- private static float GetValue(BaseItem x)
+ private static float GetValue(BaseItem? x)
{
- return x.CriticRating ?? 0;
+ return x?.CriticRating ?? 0;
}
/// <summary>
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index cbca300d2..d3f10f78c 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index 03ff19d21..b1cb123ce 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 16bd2aff8..08a44319f 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index 0c4e82d01..73e628cf7 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
index a35192eff..3c5ddeefa 100644
--- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
@@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
@@ -30,9 +30,9 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>System.String.</returns>
- private static int GetValue(BaseItem x)
+ private static int GetValue(BaseItem? x)
{
- return x.IsFolder ? 0 : 1;
+ return x?.IsFolder ?? true ? 0 : 1;
}
}
}
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index d95948406..7d77a8bc5 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index 1632c5a7a..926835f90 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index da020d8d8..4de81a69e 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index 76bb798b5..a81f78ebf 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
if (x == null)
{
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 5c2830322..04e4865cb 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index 92ac04dc6..c98f97bf1 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetDate(x).CompareTo(GetDate(y));
}
@@ -26,8 +26,13 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>DateTime.</returns>
- private static DateTime GetDate(BaseItem x)
+ private static DateTime GetDate(BaseItem? x)
{
+ if (x == null)
+ {
+ return DateTime.MinValue;
+ }
+
if (x.PremiereDate.HasValue)
{
return x.PremiereDate.Value;
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index e2857df0b..df9f9957d 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetValue(x).CompareTo(GetValue(y));
}
@@ -25,8 +25,13 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>DateTime.</returns>
- private static int GetValue(BaseItem x)
+ private static int GetValue(BaseItem? x)
{
+ if (x == null)
+ {
+ return 0;
+ }
+
if (x.ProductionYear.HasValue)
{
return x.ProductionYear.Value;
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index 7739d0418..af3bc2750 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return Guid.NewGuid().CompareTo(Guid.NewGuid());
}
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index dde44333d..129315303 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index b9205ee07..4123a59f8 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index f745e193b..8d30716d3 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index 558a3d351..c3df7c47e 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 5766dc542..01445c525 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs
index 7c2ad2477..12efff261 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 72c0a838e..993456196 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 829df64bf..af453d148 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -152,7 +154,7 @@ namespace Emby.Server.Implementations.TV
return i.Item1 != DateTime.MinValue;
}
- if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
+ if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff))
{
anyFound = true;
return true;
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index db5265e79..8179e26c5 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -18,17 +18,17 @@ namespace Emby.Server.Implementations.Udp
public sealed class UdpServer : IDisposable
{
/// <summary>
+ /// Address Override Configuration Key.
+ /// </summary>
+ public const string AddressOverrideConfigKey = "PublishedServerUrl";
+
+ /// <summary>
/// The _logger.
/// </summary>
private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
- /// <summary>
- /// Address Override Configuration Key.
- /// </summary>
- public const string AddressOverrideConfigKey = "PublishedServerUrl";
-
private Socket _udpSocket;
private IPEndPoint _endpoint;
private readonly byte[] _receiveBuffer = new byte[8192];
@@ -38,49 +38,58 @@ namespace Emby.Server.Implementations.Udp
/// <summary>
/// Initializes a new instance of the <see cref="UdpServer" /> class.
/// </summary>
- public UdpServer(ILogger logger, IServerApplicationHost appHost, IConfiguration configuration)
+ /// <param name="logger">The logger.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="configuration">The configuration manager.</param>
+ /// <param name="port">The port.</param>
+ public UdpServer(
+ ILogger logger,
+ IServerApplicationHost appHost,
+ IConfiguration configuration,
+ int port)
{
_logger = logger;
_appHost = appHost;
_config = configuration;
+
+ _endpoint = new IPEndPoint(IPAddress.Any, port);
+
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
{
- string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
- ? _config[AddressOverrideConfigKey]
- : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
+ string? localUrl = _config[AddressOverrideConfigKey];
+ if (string.IsNullOrEmpty(localUrl))
+ {
+ localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
+ }
- if (!string.IsNullOrEmpty(localUrl))
+ if (string.IsNullOrEmpty(localUrl))
{
- var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+ _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined.");
+ return;
+ }
- try
- {
- await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
- }
- catch (SocketException ex)
- {
- _logger.LogError(ex, "Error sending response message");
- }
+ var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+
+ try
+ {
+ await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
}
- else
+ catch (SocketException ex)
{
- _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined.");
+ _logger.LogError(ex, "Error sending response message");
}
}
/// <summary>
/// Starts the specified port.
/// </summary>
- /// <param name="port">The port.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
- public void Start(int port, CancellationToken cancellationToken)
+ public void Start(CancellationToken cancellationToken)
{
- _endpoint = new IPEndPoint(IPAddress.Any, port);
-
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
- _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_udpSocket.Bind(_endpoint);
_ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
@@ -88,9 +97,9 @@ namespace Emby.Server.Implementations.Udp
private async Task BeginReceiveAsync(CancellationToken cancellationToken)
{
+ var infiniteTask = Task.Delay(-1, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
- var infiniteTask = Task.Delay(-1, cancellationToken);
try
{
var task = _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 653b1381b..b0921cbd8 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -101,12 +99,12 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
- public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
+ public async Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
- List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
+ PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetFromJsonAsync<PackageInfo[]>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
if (packages == null)
{
@@ -179,20 +177,14 @@ namespace Emby.Server.Implementations.Updates
// Where repositories have the same content, the details from the first is taken.
foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{
- if (!Guid.TryParse(package.Id, out var packageGuid))
- {
- // Package doesn't have a valid GUID, skip.
- continue;
- }
-
- var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
+ var existing = FilterPackages(result, package.Name, package.Id).FirstOrDefault();
// Remove invalid versions from the valid package.
for (var i = package.Versions.Count - 1; i >= 0; i--)
{
var version = package.Versions[i];
- var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
+ var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
if (plugin != null)
{
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
@@ -231,7 +223,7 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? specificVersion = null)
{
if (name != null)
@@ -239,9 +231,9 @@ namespace Emby.Server.Implementations.Updates
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
- if (id != Guid.Empty)
+ if (id != default)
{
- availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id);
+ availablePackages = availablePackages.Where(x => x.Id == id);
}
if (specificVersion != null)
@@ -256,7 +248,7 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? minVersion = null,
Version? specificVersion = null)
{
@@ -286,7 +278,7 @@ namespace Emby.Server.Implementations.Updates
yield return new InstallationInfo
{
Changelog = v.Changelog,
- Id = new Guid(package.Id),
+ Id = package.Id,
Name = package.Name,
Version = v.VersionNumber,
SourceUrl = v.SourceUrl,
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 85d7c50d3..154a56702 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -281,6 +281,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Total record count.</param>
/// <response code="200">Album artists returned.</response>
@@ -316,6 +318,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -354,7 +358,8 @@ namespace Jellyfin.Api.Controllers
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
};
if (parentId.HasValue)
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 9f3216de3..62283d038 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1190,7 +1190,8 @@ namespace Jellyfin.Api.Controllers
throw new ArgumentException("StartTimeTicks is not allowed.");
}
- using var cancellationTokenSource = new CancellationTokenSource();
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
using var state = await StreamingHelpers.GetStreamingState(
@@ -1760,9 +1761,9 @@ namespace Jellyfin.Api.Controllers
private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
{
- var folder = Path.GetDirectoryName(playlist);
+ var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist));
- var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
+ var filePrefix = Path.GetFileNameWithoutExtension(playlist);
try
{
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 74cf3b162..35c27dd0e 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -143,7 +143,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItems(
- [FromQuery] Guid? userId,
+ [FromQuery] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
@@ -224,8 +224,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
- var user = userId.HasValue && !userId.Equals(Guid.Empty)
- ? _userManager.GetUserById(userId.Value)
+ var user = !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId)
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 2d5339b16..11f67ee89 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
- [HttpGet("Videos/{routeItemId}/routeMediaSourceId/Subtitles/{routeIndex}/Stream.{routeFormat}")]
+ [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public async Task<ActionResult> GetSubtitle(
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index dd3836551..5cb7468b2 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
- [FromQuery] Guid? userId,
+ [FromQuery] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 59400db2a..ffb726fab 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -65,6 +65,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
@@ -81,6 +82,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
+ [FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false)
{
@@ -97,7 +99,8 @@ namespace Jellyfin.Api.Controllers
StartIndex = startIndex,
UserId = userId ?? Guid.Empty,
EnableTotalRecordCount = enableTotalRecordCount,
- DisableFirstEpisode = disableFirstEpisode
+ DisableFirstEpisode = disableFirstEpisode,
+ NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue
},
options);
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 308334b23..6a720b1a4 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -265,6 +265,7 @@ namespace Jellyfin.Api.Controllers
EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
};
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index e544d001e..dc64a0f1b 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -373,6 +373,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Dictionary<string, string> streamOptions)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
{
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index cf35ee23a..264131905 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -97,6 +97,8 @@ namespace Jellyfin.Api.Helpers
}
bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
+
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
using var state = await StreamingHelpers.GetStreamingState(
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index fcada0e77..dc5d6715b 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -106,6 +106,7 @@ namespace Jellyfin.Api.Helpers
bool enableAdaptiveBitrateStreaming)
{
var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
return await GetMasterPlaylistInternal(
streamingRequest,
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 6fec42d44..e9db07d5a 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -269,7 +269,7 @@ namespace Jellyfin.Api.Helpers
{
_activeTranscodingJobs.Remove(job);
- if (!job.CancellationTokenSource!.IsCancellationRequested)
+ if (job.CancellationTokenSource?.IsCancellationRequested == false)
{
job.CancellationTokenSource.Cancel();
}
@@ -379,7 +379,9 @@ namespace Jellyfin.Api.Helpers
/// <param name="outputFilePath">The output file path.</param>
private void DeleteHlsPartialStreamFiles(string outputFilePath)
{
- var directory = Path.GetDirectoryName(outputFilePath);
+ var directory = Path.GetDirectoryName(outputFilePath)
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+
var name = Path.GetFileNameWithoutExtension(outputFilePath);
var filesToDelete = _fileSystem.GetFilePaths(directory)
@@ -749,7 +751,7 @@ namespace Jellyfin.Api.Helpers
_logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
}
- process.Dispose();
+ job.Dispose();
}
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index eb9fc4f14..bd7da9b06 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.6" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.1.4" />
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index 9edc19bb6..291e571dc 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -11,7 +11,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// <summary>
/// Class TranscodingJob.
/// </summary>
- public class TranscodingJobDto
+ public class TranscodingJobDto : IDisposable
{
/// <summary>
/// The process lock.
@@ -249,5 +249,31 @@ namespace Jellyfin.Api.Models.PlaybackDtos
}
}
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose all resources.
+ /// </summary>
+ /// <param name="disposing">Whether to dispose all resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Process?.Dispose();
+ Process = null;
+ KillTimer?.Dispose();
+ KillTimer = null;
+ CancellationTokenSource?.Dispose();
+ CancellationTokenSource = null;
+ TranscodingThrottler?.Dispose();
+ TranscodingThrottler = null;
+ }
+ }
}
}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
index e33e552ed..7b32d76ba 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
@@ -145,7 +145,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
- var path = job.Path;
+ var path = job.Path ?? throw new ArgumentException("Path can't be null.");
+
var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
diff --git a/Jellyfin.Server.Implementations/Events/EventManager.cs b/Jellyfin.Server.Implementations/Events/EventManager.cs
index 707002442..8c5d8f2ce 100644
--- a/Jellyfin.Server.Implementations/Events/EventManager.cs
+++ b/Jellyfin.Server.Implementations/Events/EventManager.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Server.Implementations.Events
public void Publish<T>(T eventArgs)
where T : EventArgs
{
- Task.WaitAll(PublishInternal(eventArgs));
+ PublishInternal(eventArgs).GetAwaiter().GetResult();
}
/// <inheritdoc />
@@ -43,7 +43,12 @@ namespace Jellyfin.Server.Implementations.Events
private async Task PublishInternal<T>(T eventArgs)
where T : EventArgs
{
- using var scope = _appHost.ServiceProvider.CreateScope();
+ using var scope = _appHost.ServiceProvider?.CreateScope();
+ if (scope == null)
+ {
+ return;
+ }
+
foreach (var service in scope.ServiceProvider.GetServices<IEventConsumer<T>>())
{
try
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index d24c73526..eeeb1d19b 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -27,13 +27,13 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.6" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.6" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.6">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.7" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.6">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 9a073c477..97704386a 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -28,7 +28,7 @@ namespace Jellyfin.Server.Implementations.Security
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached != null)
{
- return Task.FromResult((AuthorizationInfo)cached);
+ return Task.FromResult((AuthorizationInfo)cached!); // Cache should never contain null
}
return GetAuthorization(requestContext);
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 88e2b4152..e29167747 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -79,6 +79,16 @@ namespace Jellyfin.Server.Extensions
}
/// <summary>
+ /// Enables url decoding before binding to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseQueryStringDecoding(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<QueryStringDecodingMiddleware>();
+ }
+
+ /// <summary>
/// Adds base url redirection to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index f83de7ac8..ea782cb66 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -38,8 +38,8 @@
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.6" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.6" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.7" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.7" />
<PackageReference Include="prometheus-net" Version="4.1.1" />
<PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index c23da2fd6..2eef223e5 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -45,15 +45,33 @@ namespace Jellyfin.Server.Middleware
var localPath = httpContext.Request.Path.ToString();
var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
- if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
- || string.IsNullOrEmpty(localPath)
- || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(baseUrlPrefix))
{
- // Always redirect back to the default path if the base prefix is invalid or missing
+ var startsWithBaseUrl = localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase);
+
+ if (!startsWithBaseUrl
+ && (localPath.Equals("/health", StringComparison.OrdinalIgnoreCase)
+ || localPath.Equals("/health/", StringComparison.OrdinalIgnoreCase)))
+ {
+ _logger.LogDebug("Redirecting /health check");
+ httpContext.Response.Redirect(baseUrlPrefix + "/health");
+ return;
+ }
+
+ if (!startsWithBaseUrl)
+ {
+ // Always redirect back to the default path if the base prefix is invalid or missing
+ _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
+ httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ return;
+ }
+ }
+ else if (string.IsNullOrEmpty(localPath)
+ || localPath.Equals("/", StringComparison.Ordinal))
+ {
+ // Always redirect back to the default path if root is requested.
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
- httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ httpContext.Response.Redirect("/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
return;
}
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
new file mode 100644
index 000000000..fd0ebbf43
--- /dev/null
+++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// URL decodes the querystring before binding.
+ /// </summary>
+ public class QueryStringDecodingMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public QueryStringDecodingMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext)
+ {
+ httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>()));
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
new file mode 100644
index 000000000..310a3d31a
--- /dev/null
+++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using MediaBrowser.Common.Extensions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Defines the <see cref="UrlDecodeQueryFeature"/>.
+ /// </summary>
+ public class UrlDecodeQueryFeature : IQueryFeature
+ {
+ private IQueryCollection? _store;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class.
+ /// </summary>
+ /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param>
+ public UrlDecodeQueryFeature(IQueryFeature feature)
+ {
+ Query = feature.Query;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>.
+ /// </summary>
+ public IQueryCollection Query
+ {
+ get
+ {
+ return _store ?? QueryCollection.Empty;
+ }
+
+ set
+ {
+ // Only interested in where the querystring is encoded which shows up as one key with nothing in the value.
+ if (value.Count != 1)
+ {
+ _store = value;
+ return;
+ }
+
+ // Encoded querystrings have no value, so don't process anything if a value is present.
+ var (key, stringValues) = value.First();
+ if (!string.IsNullOrEmpty(stringValues))
+ {
+ _store = value;
+ return;
+ }
+
+ // Unencode and re-parse querystring.
+ var unencodedKey = HttpUtility.UrlDecode(key);
+
+ if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
+ {
+ // Don't do anything if it's not encoded.
+ _store = value;
+ return;
+ }
+
+ var pairs = new Dictionary<string, StringValues>();
+ var queryString = unencodedKey.SpanSplit('&');
+
+ foreach (var pair in queryString)
+ {
+ var i = pair.IndexOf('=');
+ if (i == -1)
+ {
+ // encoded is an equals.
+ // We use TryAdd so duplicate keys get ignored
+ pairs.TryAdd(pair.ToString(), StringValues.Empty);
+ continue;
+ }
+
+ var k = pair[..i].ToString();
+ var v = pair[(i + 1)..].ToString();
+ if (!pairs.TryAdd(k, new StringValues(v)))
+ {
+ pairs[k] = StringValues.Concat(pairs[k], v);
+ }
+ }
+
+ _store = new QueryCollection(pairs);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index a15a38177..96bd2ccc4 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -82,11 +82,14 @@ namespace Jellyfin.Server.Migrations.Routines
var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
- var config = File.Exists(Path.Combine(userDataDir, "config.xml"))
- ? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml"))
+ var configPath = Path.Combine(userDataDir, "config.xml");
+ var config = File.Exists(configPath)
+ ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration()
: new UserConfiguration();
- var policy = File.Exists(Path.Combine(userDataDir, "policy.xml"))
- ? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml"))
+
+ var policyPath = Path.Combine(userDataDir, "policy.xml");
+ var policy = File.Exists(policyPath)
+ ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy()
: new UserPolicy();
policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
"Emby.Server.Implementations.Library",
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index c10b2ddb3..3a3d7415b 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -12,10 +12,12 @@ using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
using Emby.Server.Implementations.IO;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -220,6 +222,14 @@ namespace Jellyfin.Server
}
finally
{
+ _logger.LogInformation("Running query planner optimizations in the database... This might take a while");
+ // Run before disposing the application
+ using var context = new JellyfinDbProvider(appHost.ServiceProvider, appPaths).CreateContext();
+ if (context.Database.IsSqlite())
+ {
+ context.Database.ExecuteSqlRaw("PRAGMA optimize");
+ }
+
appHost.Dispose();
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index f75139884..60cdc2f6f 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -160,6 +160,7 @@ namespace Jellyfin.Server
mainApp.UseAuthentication();
mainApp.UseJellyfinApiSwagger(_serverConfigurationManager);
+ mainApp.UseQueryStringDecoding();
mainApp.UseRouting();
mainApp.UseAuthorization();
diff --git a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs
new file mode 100644
index 000000000..75d654f23
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Extension methods for the <see cref="StringBuilder"/> class.
+ /// </summary>
+ public static class StringBuilderExtensions
+ {
+ /// <summary>
+ /// Concatenates and appends the members of a collection in single quotes using the specified delimiter.
+ /// </summary>
+ /// <param name="builder">The string builder.</param>
+ /// <param name="delimiter">The character delimiter.</param>
+ /// <param name="values">The collection of strings to concatenate.</param>
+ /// <returns>The updated string builder.</returns>
+ public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList<string> values)
+ {
+ var len = values.Count;
+ for (var i = 0; i < len; i++)
+ {
+ builder.Append('\'')
+ .Append(values[i])
+ .Append('\'')
+ .Append(delimiter);
+ }
+
+ // remove last ,
+ builder.Length--;
+
+ return builder;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 46d93e494..192a77611 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Reflection;
@@ -12,7 +10,7 @@ namespace MediaBrowser.Common
/// </summary>
/// <param name="type">Type to create.</param>
/// <returns>New instance of type <param>type</param>.</returns>
- public delegate object CreationDelegateFactory(Type type);
+ public delegate object? CreationDelegateFactory(Type type);
/// <summary>
/// An interface to be implemented by the applications hosting a kernel.
@@ -22,7 +20,7 @@ namespace MediaBrowser.Common
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
- event EventHandler HasPendingRestartChanged;
+ event EventHandler? HasPendingRestartChanged;
/// <summary>
/// Gets the name.
@@ -63,7 +61,7 @@ namespace MediaBrowser.Common
/// <summary>
/// Gets or sets the service provider.
/// </summary>
- IServiceProvider ServiceProvider { get; set; }
+ IServiceProvider? ServiceProvider { get; set; }
/// <summary>
/// Gets the application version.
diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
index c2a28e0a2..458494bdc 100644
--- a/MediaBrowser.Common/Updates/IInstallationManager.cs
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Updates
/// <param name="filterIncompatible">Filter out incompatible plugins.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
- Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
+ Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all available packages that are supported by this version.
@@ -45,7 +45,7 @@ namespace MediaBrowser.Common.Updates
IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? specificVersion = null);
/// <summary>
@@ -60,7 +60,7 @@ namespace MediaBrowser.Common.Updates
IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? minVersion = null,
Version? specificVersion = null);
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index 68119cfed..ffc274c5d 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -52,7 +53,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
- return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
if (!libraryOptions.EnableInternetProviders)
@@ -62,7 +63,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
- return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -83,7 +84,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
- return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
if (!libraryOptions.EnableInternetProviders)
@@ -93,7 +94,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
- return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
diff --git a/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
index a2dc5682d..0539b9048 100644
--- a/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
+++ b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
@@ -1,8 +1,12 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.Channels
+namespace MediaBrowser.Controller.Channels
{
+ /// <summary>
+ /// Disable media source display.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="Channel"/> can inherit this interface to disable being displayed.
+ /// </remarks>
public interface IDisableMediaSourceDisplay
{
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
index 2682de51c..bc7683125 100644
--- a/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
+++ b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
@@ -1,8 +1,9 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.Channels
+namespace MediaBrowser.Controller.Channels
{
+ /// <summary>
+ /// Channel supports media probe.
+ /// </summary>
public interface ISupportsMediaProbe
{
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index 800f7a8bb..4e640d421 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -1,7 +1,4 @@
-#nullable disable
-
#pragma warning disable CS1591
-#nullable enable
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index 9bfead8b3..c7f61a90b 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -1,7 +1,4 @@
-#nullable disable
-
#pragma warning disable CS1591
-#nullable enable
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs
index 204175ed5..9ef92bc98 100644
--- a/MediaBrowser.Controller/Drawing/ImageHelper.cs
+++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs
@@ -1,7 +1,4 @@
-#nullable disable
-
#pragma warning disable CS1591
-#nullable enable
using MediaBrowser.Model.Drawing;
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index 533130fc8..fe1bc62ab 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -155,11 +155,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
@@ -185,7 +185,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
+ /// <exception cref="ArgumentNullException">The id is empty.</exception>
public BaseItem FindVirtualChild(Guid id)
{
if (id.Equals(Guid.Empty))
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index 4c2b7cb7c..576ab67a2 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
@@ -82,19 +83,19 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "")
- + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty)
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
}
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
- var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty;
+ var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : string.Empty;
if (ParentIndexNumber.HasValue)
{
- songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey;
+ songKey = ParentIndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) + "-" + songKey;
}
songKey += Name;
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 0928a8073..c0cd81110 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -94,7 +94,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return base.IsSaveLocalMetadataEnabled();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (IsAccessedByName)
{
@@ -102,7 +102,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return Task.CompletedTask;
}
- return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
+ return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken);
}
public override List<string> GetUserDataKeys()
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 6e46b4cec..6137ddbf7 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -620,7 +620,11 @@ namespace MediaBrowser.Controller.Entities
public string ForcedSortName
{
get => _forcedSortName;
- set { _forcedSortName = value; _sortName = null; }
+ set
+ {
+ _forcedSortName = value;
+ _sortName = null;
+ }
}
private string _sortName;
@@ -666,14 +670,12 @@ namespace MediaBrowser.Controller.Entities
{
if (SourceType == SourceType.Channel)
{
- return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
+ return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
}
ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
- basePath = System.IO.Path.Combine(basePath, "library");
-
- return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString);
+ return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString);
}
/// <summary>
@@ -1258,7 +1260,7 @@ namespace MediaBrowser.Controller.Entities
// Support plex/xbmc convention
files.AddRange(fileSystemChildren
- .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
+ .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
.OfType<Audio.Audio>()
@@ -1319,14 +1321,16 @@ namespace MediaBrowser.Controller.Entities
{
var extras = new List<Video>();
- var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray();
+ var libraryOptions = new LibraryOptions();
+ var folders = fileSystemChildren.Where(i => i.IsDirectory).ToList();
foreach (var extraFolderName in AllExtrasTypesFolderNames)
{
var files = folders
.Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => FileSystem.GetFiles(i.FullName));
- extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
+ // Re-using the same instance of LibraryOptions since it looks like it's never being altered.
+ extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, libraryOptions)
.OfType<Video>()
.Select(item =>
{
@@ -1337,7 +1341,7 @@ namespace MediaBrowser.Controller.Entities
}
// Use some hackery to get the extra type based on foldername
- item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty), true, out ExtraType extraType)
+ item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty, StringComparison.Ordinal), true, out ExtraType extraType)
? extraType
: Model.Entities.ExtraType.Unknown;
@@ -1427,10 +1431,10 @@ namespace MediaBrowser.Controller.Entities
/// Refreshes owned items such as trailers, theme videos, special features, etc.
/// Returns true or false indicating if changes were found.
/// </summary>
- /// <param name="options"></param>
- /// <param name="fileSystemChildren"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="options">The metadata refresh options.</param>
+ /// <param name="fileSystemChildren">The list of filesystem children.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var themeSongsChanged = false;
@@ -1772,7 +1776,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="user">The user.</param>
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
+ /// <exception cref="ArgumentNullException">If user is null.</exception>
public bool IsParentalAllowed(User user)
{
if (user == null)
@@ -1917,7 +1921,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="user">The user.</param>
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
public virtual bool IsVisible(User user)
{
if (user == null)
@@ -2215,7 +2219,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="type">The type.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
+ /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
public bool HasImage(ImageType type, int imageIndex)
{
return GetImageInfo(type, imageIndex) != null;
@@ -2323,7 +2327,7 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i.IsLocalFile)
.Select(i => System.IO.Path.GetDirectoryName(i.Path))
.Distinct(StringComparer.OrdinalIgnoreCase)
- .SelectMany(directoryService.GetFilePaths)
+ .SelectMany(path => directoryService.GetFilePaths(path))
.ToList();
var deletedImages = ImageInfos
@@ -2344,9 +2348,8 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns>System.String.</returns>
- /// <exception cref="InvalidOperationException">
- /// </exception>
- /// <exception cref="ArgumentNullException">item</exception>
+ /// <exception cref="InvalidOperationException"> </exception>
+ /// <exception cref="ArgumentNullException">Item is null.</exception>
public string GetImagePath(ImageType imageType, int imageIndex)
=> GetImageInfo(imageType, imageIndex)?.Path;
@@ -2433,7 +2436,15 @@ namespace MediaBrowser.Controller.Entities
throw new ArgumentException("No image info for chapter images");
}
- return ImageInfos.Where(i => i.Type == imageType);
+ // Yield return is more performant than LINQ Where on an Array
+ for (var i = 0; i < ImageInfos.Length; i++)
+ {
+ var imageInfo = ImageInfos[i];
+ if (imageInfo.Type == imageType)
+ {
+ yield return imageInfo;
+ }
+ }
}
/// <summary>
@@ -2442,7 +2453,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="images">The images.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
- /// <exception cref="ArgumentException">Cannot call AddImages with chapter images</exception>
+ /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
{
if (imageType == ImageType.Chapter)
@@ -2465,7 +2476,7 @@ namespace MediaBrowser.Controller.Entities
}
var existing = existingImages
- .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
+ .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
@@ -2496,8 +2507,7 @@ namespace MediaBrowser.Controller.Entities
var newImagePaths = images.Select(i => i.FullName).ToList();
var deleted = existingImages
- .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path))
- .ToList();
+ .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path));
if (deleted.Count > 0)
{
@@ -2526,10 +2536,11 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the file system path to delete when the item is to be deleted.
/// </summary>
- /// <returns></returns>
+ /// <returns>The metadata for the deleted paths.</returns>
public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = Path,
@@ -2636,6 +2647,7 @@ namespace MediaBrowser.Controller.Entities
MetadataCountryCode = GetPreferredMetadataCountryCode(),
MetadataLanguage = GetPreferredMetadataLanguage(),
Name = GetNameForMetadataLookup(),
+ OriginalTitle = OriginalTitle,
ProviderIds = ProviderIds,
IndexNumber = IndexNumber,
ParentIndexNumber = ParentIndexNumber,
@@ -2889,7 +2901,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Updates the official rating based on content and returns true or false indicating if it changed.
/// </summary>
- /// <returns></returns>
+ /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
public bool UpdateRatingToItems(IList<BaseItem> children)
{
var currentOfficialRating = OfficialRating;
@@ -2905,7 +2917,9 @@ namespace MediaBrowser.Controller.Entities
OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
- return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty,
+ return !string.Equals(
+ currentOfficialRating ?? string.Empty,
+ OfficialRating ?? string.Empty,
StringComparison.OrdinalIgnoreCase);
}
@@ -3002,7 +3016,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- public bool Equals(BaseItem item) => Object.Equals(Id, item?.Id);
+ public bool Equals(BaseItem other) => object.Equals(Id, other?.Id);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(Id);
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index c39b18891..89ad392a4 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -1,6 +1,3 @@
-#nullable disable
-
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index d0fb3997d..4a721ca44 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -315,16 +315,16 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
- /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+ /// ***Currently does not contain logic to maintain items that are unavailable in the file system***.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 29d837c14..541747422 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -207,8 +207,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="InvalidOperationException">Unable to add + item.Name</exception>
+ /// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception>
public void AddChild(BaseItem item, CancellationToken cancellationToken)
{
item.SetParent(this);
@@ -274,20 +273,20 @@ namespace MediaBrowser.Controller.Entities
public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
{
- return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem)));
+ return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken: cancellationToken);
}
/// <summary>
/// Validates that the children of the folder still exist.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="metadataRefreshOptions">The metadata refresh options.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true)
+ public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, CancellationToken cancellationToken = default)
{
- return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
+ return ValidateChildrenInternal(progress, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
}
private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
@@ -327,13 +326,13 @@ namespace MediaBrowser.Controller.Entities
/// Validates the children internal.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (recursive)
{
@@ -342,7 +341,7 @@ namespace MediaBrowser.Controller.Entities
try
{
- await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false);
+ await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
}
finally
{
@@ -353,7 +352,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -575,7 +574,7 @@ namespace MediaBrowser.Controller.Entities
private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
{
return RunTasks(
- (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService),
+ (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, null, directoryService, cancellationToken),
children,
progress,
cancellationToken);
@@ -988,7 +987,13 @@ namespace MediaBrowser.Controller.Entities
}
else
{
- items = GetChildren(user, true).Where(filter);
+ // need to pass this param to the children.
+ var childQuery = new InternalItemsQuery
+ {
+ DisplayAlbumFolders = query.DisplayAlbumFolders
+ };
+
+ items = GetChildren(user, true, childQuery).Where(filter);
}
return PostFilterAndSort(items, query, true);
@@ -1013,7 +1018,7 @@ namespace MediaBrowser.Controller.Entities
if (!string.IsNullOrEmpty(query.NameStartsWith))
{
- items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase));
+ items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.CurrentCultureIgnoreCase));
}
if (!string.IsNullOrEmpty(query.NameLessThan))
@@ -1324,10 +1329,23 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the children to list.
/// </summary>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query)
{
- foreach (var child in GetEligibleChildrenForRecursiveChildren(user))
+ // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
+ IEnumerable<BaseItem> children = null;
+ if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
+ {
+ children = Children;
+ query = null;
+ }
+
+ // If there are not sub-folders, proceed as normal.
+ if (children == null)
+ {
+ children = GetEligibleChildrenForRecursiveChildren(user);
+ }
+
+ foreach (var child in children)
{
bool? isVisibleToUser = null;
@@ -1596,7 +1614,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Refreshes the linked children.
/// </summary>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
+ /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
{
if (SupportsShortcutChildren)
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index 698643b44..b80a5be3b 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -107,7 +107,11 @@ namespace MediaBrowser.Controller.Entities
return base.RequiresRefresh();
}
- /// <inheridoc />
+ /// <summary>
+ /// This is called before any metadata refresh and returns true if changes were made.
+ /// </summary>
+ /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
+ /// <returns>true if the item has change, else false.</returns>
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
index b027a0cb1..ae01c223e 100644
--- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs
+++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
@@ -1,7 +1,7 @@
namespace MediaBrowser.Controller.Entities
{
/// <summary>
- /// Interface IHasScreenshots.
+ /// The item has screenshots.
/// </summary>
public interface IHasScreenshots
{
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 75fea365b..ebaf5506d 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -265,6 +265,11 @@ namespace MediaBrowser.Controller.Entities
public bool? IsDeadPerson { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether album sub-folders should be returned if they exist.
+ /// </summary>
+ public bool? DisplayAlbumFolders { get; set; }
+
public InternalItemsQuery()
{
AlbumArtistIds = Array.Empty<Guid>();
diff --git a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
index 66fc44b8a..4e58e2942 100644
--- a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
+using System;
using System.Collections.Generic;
using MediaBrowser.Model.IO;
@@ -28,7 +29,7 @@ namespace MediaBrowser.Controller.Entities
public int GetHashCode(LinkedChild obj)
{
- return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode();
+ return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal);
}
}
} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 1b4cc7a78..31c179bca 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -218,8 +218,8 @@ namespace MediaBrowser.Controller.Entities.TV
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "")
- + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty)
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
}
/// <summary>
@@ -287,7 +287,8 @@ namespace MediaBrowser.Controller.Entities.TV
public override IEnumerable<FileSystemMetadata> GetDeletePaths()
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = Path,
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 5e2053dcc..aa62bb35b 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -122,7 +122,7 @@ namespace MediaBrowser.Controller.Entities.TV
var series = Series;
if (series != null)
{
- return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000");
+ return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture);
}
}
@@ -135,7 +135,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name;
+ return IndexNumber != null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name;
}
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index 2dea2e50b..2b15a52f0 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -109,11 +109,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index 1e6c01bf8..57dc9b59b 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -172,7 +172,7 @@ namespace MediaBrowser.Controller.Entities
return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 15a4573c2..add734f62 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -55,12 +55,12 @@ namespace MediaBrowser.Controller.Entities
// if (query.IncludeItemTypes != null &&
// query.IncludeItemTypes.Length == 1 &&
// string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase))
- //{
+ // {
// if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
// {
// return await FindPlaylists(queryParent, user, query).ConfigureAwait(false);
// }
- //}
+ // }
switch (viewType)
{
@@ -344,12 +344,14 @@ namespace MediaBrowser.Controller.Entities
var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty });
var result = _tvSeriesManager.GetNextUp(
- new NextUpQuery
- {
- Limit = query.Limit,
- StartIndex = query.StartIndex,
- UserId = query.User.Id
- }, parentFolders, query.DtoOptions);
+ new NextUpQuery
+ {
+ Limit = query.Limit,
+ StartIndex = query.StartIndex,
+ UserId = query.User.Id
+ },
+ parentFolders,
+ query.DtoOptions);
return result;
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 723027a88..d05b5df2f 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -482,7 +482,8 @@ namespace MediaBrowser.Controller.Entities
{
if (!IsInMixedFolder)
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = ContainingFolderPath,
diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs
index 48bd9522a..f1af01345 100644
--- a/MediaBrowser.Controller/Extensions/StringExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs
@@ -21,6 +21,27 @@ namespace MediaBrowser.Controller.Extensions
return Normalize(string.Concat(chars), NormalizationForm.FormC);
}
+ /// <summary>
+ /// Counts the number of occurrences of [needle] in the string.
+ /// </summary>
+ /// <param name="value">The haystack to search in.</param>
+ /// <param name="needle">The character to search for.</param>
+ /// <returns>The number of occurrences of the [needle] character.</returns>
+ public static int Count(this ReadOnlySpan<char> value, char needle)
+ {
+ var count = 0;
+ var length = value.Length;
+ for (var i = 0; i < length; i++)
+ {
+ if (value[i] == needle)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true)
{
if (stripStringOnFailure)
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
index 3db60ae0b..b8a0bf331 100644
--- a/MediaBrowser.Controller/IO/FileData.cs
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.IO
/// <param name="flattenFolderDepth">The flatten folder depth.</param>
/// <param name="resolveShortcuts">if set to <c>true</c> [resolve shortcuts].</param>
/// <returns>Dictionary{System.StringFileSystemInfo}.</returns>
- /// <exception cref="ArgumentNullException">path</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c> or empty.</exception>
public static FileSystemMetadata[] GetFilteredFileSystemEntries(
IDirectoryService directoryService,
string path,
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 3fd4ff899..7a4ba6a24 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Naming.Common;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -352,6 +353,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
/// <param name="uniqueId">The unique identifier.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
string name,
Guid parentId,
@@ -365,10 +367,11 @@ namespace MediaBrowser.Controller.Library
/// <param name="parent">The parent.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The shadow view.</returns>
UserView GetShadowView(
BaseItem parent,
- string viewType,
- string sortName);
+ string viewType,
+ string sortName);
/// <summary>
/// Determines whether [is video file] [the specified path].
@@ -593,5 +596,11 @@ namespace MediaBrowser.Controller.Library
BaseItem GetParentItem(string parentId, Guid? userId);
BaseItem GetParentItem(Guid? parentId, Guid? userId);
+
+ /// <summary>
+ /// Gets or creates a static instance of <see cref="NamingOptions"/>.
+ /// </summary>
+ /// <returns>An instance of the <see cref="NamingOptions"/> class.</returns>
+ NamingOptions GetNamingOptions();
}
}
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index 58499e853..e5dcfcff0 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -49,17 +49,16 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Get all user data for the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <returns>The user item data.</returns>
List<UserItemData> GetAllUserData(Guid userId);
/// <summary>
/// Save the all provided user data for the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <param name="userData"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userData">The array of user data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken);
/// <summary>
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 3f70a62cc..51118e39c 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -61,8 +61,8 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <param name="newName">The new name.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
Task RenameUser(User user, string newName);
/// <summary>
@@ -79,8 +79,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="name">The name of the new user.</param>
/// <returns>The created user.</returns>
- /// <exception cref="ArgumentNullException">name</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception>
+ /// <exception cref="ArgumentException"><paramref name="name"/> already exists.</exception>
Task<User> CreateUserAsync(string name);
/// <summary>
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 0e2d8fb02..521e37274 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -39,7 +39,7 @@ namespace MediaBrowser.Controller.Library
public IDirectoryService DirectoryService { get; }
/// <summary>
- /// Gets the file system children.
+ /// Gets or sets the file system children.
/// </summary>
/// <value>The file system children.</value>
public FileSystemMetadata[] FileSystemChildren { get; set; }
@@ -242,14 +242,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
public override int GetHashCode()
{
- return Path.GetHashCode();
+ return Path.GetHashCode(StringComparison.Ordinal);
}
/// <summary>
/// Equals the specified args.
/// </summary>
/// <param name="args">The args.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <returns><c>true</c> if the arguments are the same, <c>false</c> otherwise.</returns>
protected bool Equals(ItemResolveArgs args)
{
if (args != null)
diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs
deleted file mode 100644
index 583fd73c3..000000000
--- a/MediaBrowser.Controller/Library/Profiler.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-#nullable disable
-
-using System;
-using System.Diagnostics;
-using System.Globalization;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Controller.Library
-{
- /// <summary>
- /// Class Profiler.
- /// </summary>
- public class Profiler : IDisposable
- {
- /// <summary>
- /// The name.
- /// </summary>
- private readonly string _name;
-
- /// <summary>
- /// The stopwatch.
- /// </summary>
- private readonly Stopwatch _stopwatch;
-
- /// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger<Profiler> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Profiler" /> class.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <param name="logger">The logger.</param>
- public Profiler(string name, ILogger<Profiler> logger)
- {
- this._name = name;
-
- _logger = logger;
-
- _stopwatch = new Stopwatch();
- _stopwatch.Start();
- }
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- _stopwatch.Stop();
- string message;
- if (_stopwatch.ElapsedMilliseconds > 300000)
- {
- message = string.Format(
- CultureInfo.InvariantCulture,
- "{0} took {1} minutes.",
- _name,
- ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F", CultureInfo.InvariantCulture));
- }
- else
- {
- message = string.Format(
- CultureInfo.InvariantCulture,
- "{0} took {1} seconds.",
- _name,
- ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000", CultureInfo.InvariantCulture));
- }
-
- _logger.LogInformation(message);
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index e2adec000..9d638a0bf 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -53,6 +53,7 @@ namespace MediaBrowser.Controller.LiveTv
}
private static string EmbyServiceName = "Emby";
+
public override double GetDefaultPrimaryImageAspectRatio()
{
var serviceName = ServiceName;
@@ -150,14 +151,14 @@ namespace MediaBrowser.Controller.LiveTv
[JsonIgnore]
public override string ContainingFolderPath => Path;
- //[JsonIgnore]
+ // [JsonIgnore]
// public override string MediaType
- //{
+ // {
// get
// {
// return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio;
// }
- //}
+ // }
[JsonIgnore]
public bool IsAiring
diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
index 728387c56..92eb0be9c 100644
--- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
@@ -1,6 +1,3 @@
-#nullable disable
-
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 3f3a505ea..ee76ff080 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -21,8 +21,9 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" />
+ <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
+ <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 61f3bc771..745ee6bdb 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -1,4 +1,4 @@
-#nullable disable
+#nullable disable
#pragma warning disable CS1591
@@ -8,7 +8,6 @@ using MediaBrowser.Model.Dlna;
namespace MediaBrowser.Controller.MediaEncoding
{
- // For now until api and media encoding layers are unified
public class BaseEncodingJobOptions
{
/// <summary>
@@ -202,4 +201,4 @@ namespace MediaBrowser.Controller.MediaEncoding
StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
-}
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index feb5883e5..26b0bc3de 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1166,7 +1166,9 @@ namespace MediaBrowser.Controller.MediaEncoding
profileScore = Math.Min(profileScore, 2);
// http://www.webmproject.org/docs/encoder-parameters/
- param += string.Format(CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
+ param += string.Format(
+ CultureInfo.InvariantCulture,
+ " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
profileScore.ToString(_usCulture),
crf,
qmin,
@@ -1296,7 +1298,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hevc_qsv use -level 51 instead of -level 153.
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
{
- param += " -level " + hevcLevel / 3;
+ param += " -level " + (hevcLevel / 3);
}
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
@@ -1392,7 +1394,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedProfile = requestedProfiles[0];
// strip spaces because they may be stripped out on the query string as well
if (!string.IsNullOrEmpty(videoStream.Profile)
- && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", "", StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
+ && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", string.Empty, StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
{
var currentScore = GetVideoProfileScore(videoStream.Profile);
var requestedScore = GetVideoProfileScore(requestedProfile);
@@ -1801,7 +1803,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (isTranscodingAudio
&& state.TranscodingType != TranscodingJobType.Progressive
&& resultChannels.HasValue
- && (resultChannels.Value > 2 && resultChannels.Value < 6 || resultChannels.Value == 7))
+ && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7))
{
resultChannels = 2;
}
@@ -2129,8 +2131,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return (null, null);
}
- decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth);
- decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight);
+ decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth, CultureInfo.InvariantCulture);
+ decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight, CultureInfo.InvariantCulture);
decimal outputWidth = requestedWidth.HasValue ? Convert.ToDecimal(requestedWidth.Value) : inputWidth;
decimal outputHeight = requestedHeight.HasValue ? Convert.ToDecimal(requestedHeight.Value) : inputHeight;
decimal maximumWidth = requestedMaxWidth.HasValue ? Convert.ToDecimal(requestedMaxWidth.Value) : outputWidth;
@@ -2197,12 +2199,11 @@ namespace MediaBrowser.Controller.MediaEncoding
var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
var isTonemappingSupported = IsTonemappingSupported(state, options);
var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
- var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)&& isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
+ var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported))
|| (isTonemappingSupportedOnQsv && isVppTonemappingSupported);
-
var outputPixFmt = "format=nv12";
if (isP010PixFmtRequired)
{
@@ -3175,8 +3176,8 @@ namespace MediaBrowser.Controller.MediaEncoding
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
if (state.ReadInputAtNativeFramerate
- || mediaSource.Protocol == MediaProtocol.File
- && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
+ || (mediaSource.Protocol == MediaProtocol.File
+ && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)))
{
state.InputVideoSync = "-1";
state.InputAudioSync = "1";
@@ -3549,7 +3550,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets a hw decoder name
+ /// Gets a hw decoder name.
/// </summary>
public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10)
{
@@ -3567,7 +3568,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system
+ /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system.
/// </summary>
public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10)
{
@@ -3693,7 +3694,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (flags.Count > 0)
{
- return " -fflags " + string.Join("", flags);
+ return " -fflags " + string.Join(string.Empty, flags);
}
return string.Empty;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index bc34785ee..bc0318ad7 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -69,6 +69,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
private TranscodeReason[] _transcodeReasons = null;
+
public TranscodeReason[] TranscodeReasons
{
get
@@ -274,6 +275,16 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetRequestedAudioChannels(string codec)
{
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "audiochannels");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
if (BaseRequest.MaxAudioChannels.HasValue)
{
return BaseRequest.MaxAudioChannels;
@@ -289,16 +300,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return BaseRequest.TranscodingMaxAudioChannels;
}
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "audiochannels");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
return null;
}
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index aa5e2c403..b23c95112 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -6,6 +6,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 855467e8e..d8995ce74 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -270,13 +270,4 @@ namespace MediaBrowser.Controller.Net
GC.SuppressFinalize(this);
}
}
-
- public class WebSocketListenerState
- {
- public DateTime DateLastSendUtc { get; set; }
-
- public long InitialDelayMs { get; set; }
-
- public long IntervalMs { get; set; }
- }
}
diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs
index 57938da1f..b48181b3f 100644
--- a/MediaBrowser.Controller/Net/ISessionContext.cs
+++ b/MediaBrowser.Controller/Net/ISessionContext.cs
@@ -11,10 +11,10 @@ namespace MediaBrowser.Controller.Net
{
Task<SessionInfo> GetSession(object requestContext);
- Task<User> GetUser(object requestContext);
+ Task<User?> GetUser(object requestContext);
Task<SessionInfo> GetSession(HttpContext requestContext);
- Task<User> GetUser(HttpContext requestContext);
+ Task<User?> GetUser(HttpContext requestContext);
}
}
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index e50cd9781..c8c5caf80 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -1,9 +1,5 @@
-#nullable disable
-
#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Net;
using System.Net.WebSockets;
@@ -60,11 +56,11 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// Sends a message asynchronously.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of websocket message data.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">message</exception>
+ /// <exception cref="ArgumentNullException">The message is null.</exception>
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
Task ProcessAsync(CancellationToken cancellationToken = default);
diff --git a/MediaBrowser.Controller/Net/WebSocketListenerState.cs b/MediaBrowser.Controller/Net/WebSocketListenerState.cs
new file mode 100644
index 000000000..70604d60a
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketListenerState.cs
@@ -0,0 +1,17 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Controller.Net
+{
+ public class WebSocketListenerState
+ {
+ public DateTime DateLastSendUtc { get; set; }
+
+ public long InitialDelayMs { get; set; }
+
+ public long IntervalMs { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 56fb36af2..0a9073e7f 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -49,21 +49,23 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Gets chapters for an item.
/// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
+ /// <param name="id">The item.</param>
+ /// <returns>The list of chapter info.</returns>
List<ChapterInfo> GetChapters(BaseItem id);
/// <summary>
/// Gets a single chapter for an item.
/// </summary>
- /// <param name="id"></param>
- /// <param name="index"></param>
- /// <returns></returns>
+ /// <param name="id">The item.</param>
+ /// <param name="index">The chapter index.</param>
+ /// <returns>The chapter info at the specified index.</returns>
ChapterInfo GetChapter(BaseItem id, int index);
/// <summary>
/// Saves the chapters.
/// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="chapters">The list of chapters to save.</param>
void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters);
/// <summary>
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
index 6f5f02123..5fa5834c8 100644
--- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
@@ -40,17 +40,16 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Return all user data associated with the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <returns>The list of user item data.</returns>
List<UserItemData> GetAllUserData(long userId);
/// <summary>
/// Save all user data associated with the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <param name="userData"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userData">The user item data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index bb9e5da1e..3eaf23515 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -101,7 +101,7 @@ namespace MediaBrowser.Controller.Playlists
return new List<BaseItem>();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
index ea966c282..2b831103a 100644
--- a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
+++ b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
@@ -6,4 +6,4 @@
public interface IRunBeforeStartup
{
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 291a26883..b31270270 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -25,15 +25,16 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
- return _cache.GetOrAdd(path, p => _fileSystem.GetFileSystemEntries(p).ToArray());
+ return _cache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
}
public List<FileSystemMetadata> GetFiles(string path)
{
var list = new List<FileSystemMetadata>();
var items = GetFileSystemEntries(path);
- foreach (var item in items)
+ for (var i = 0; i < items.Length; i++)
{
+ var item = items[i];
if (!item.IsDirectory)
{
list.Add(item);
@@ -48,10 +49,9 @@ namespace MediaBrowser.Controller.Providers
if (!_fileCache.TryGetValue(path, out var result))
{
var file = _fileSystem.GetFileInfo(path);
- var res = file != null && file.Exists ? file : null;
- if (res != null)
+ if (file.Exists)
{
- result = res;
+ result = file;
_fileCache.TryAdd(path, result);
}
}
@@ -62,14 +62,21 @@ namespace MediaBrowser.Controller.Providers
public IReadOnlyList<string> GetFilePaths(string path)
=> GetFilePaths(path, false);
- public IReadOnlyList<string> GetFilePaths(string path, bool clearCache)
+ public IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false)
{
if (clearCache)
{
_filePathCache.TryRemove(path, out _);
}
- return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(p).ToList());
+ var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem);
+
+ if (sort)
+ {
+ filePaths.Sort();
+ }
+
+ return filePaths;
}
}
}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index 9cee06a4c..b1a36e102 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -15,6 +15,6 @@ namespace MediaBrowser.Controller.Providers
IReadOnlyList<string> GetFilePaths(string path);
- IReadOnlyList<string> GetFilePaths(string path, bool clearCache);
+ IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
}
}
diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
index e6f49c26a..2fd89e3bb 100644
--- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
@@ -23,6 +23,12 @@ namespace MediaBrowser.Controller.Providers
public string Name { get; set; }
/// <summary>
+ /// Gets or sets the original title
+ /// </summary>
+ /// <value>The original title of the item.</value>
+ public string OriginalTitle { get; set; }
+
+ /// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs
index 8b0967a6e..7ec1eefcd 100644
--- a/MediaBrowser.Controller/Providers/MetadataResult.cs
+++ b/MediaBrowser.Controller/Providers/MetadataResult.cs
@@ -12,16 +12,26 @@ namespace MediaBrowser.Controller.Providers
{
public class MetadataResult<T>
{
+ // Images aren't always used so the allocation is a waste a lot of the time
+ private List<LocalImageInfo> _images;
+ private List<(string url, ImageType type)> _remoteImages;
+
public MetadataResult()
{
- Images = new List<LocalImageInfo>();
- RemoteImages = new List<(string url, ImageType type)>();
ResultLanguage = "en";
}
- public List<LocalImageInfo> Images { get; set; }
+ public List<LocalImageInfo> Images
+ {
+ get => _images ??= new List<LocalImageInfo>();
+ set => _images = value;
+ }
- public List<(string url, ImageType type)> RemoteImages { get; set; }
+ public List<(string url, ImageType type)> RemoteImages
+ {
+ get => _remoteImages ??= new List<(string url, ImageType type)>();
+ set => _remoteImages = value;
+ }
public List<UserItemData> UserDataList { get; set; }
diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index e77593a03..7fd54fcc6 100644
--- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Resolvers
/// <summary>
/// Class ItemResolver.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of BaseItem.</typeparam>
public abstract class ItemResolver<T> : IItemResolver
where T : BaseItem, new()
{
diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
index 4d9b98889..e00cadca2 100644
--- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
+++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
@@ -121,7 +121,9 @@ namespace MediaBrowser.Controller.Sorting
return result;
}
}
+#pragma warning disable SA1500 // TODO remove with StyleCop.Analyzers v1.2.0 https://github.com/DotNetAnalyzers/StyleCopAnalyzers/pull/3196
} while (pos1 < len1 && pos2 < len2);
+#pragma warning restore SA1500
return len1 - len2;
}
diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
index 727cbe639..07fe1ea8a 100644
--- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
@@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Sorting
/// <summary>
/// Interface IBaseItemComparer.
/// </summary>
- public interface IBaseItemComparer : IComparer<BaseItem>
+ public interface IBaseItemComparer : IComparer<BaseItem?>
{
/// <summary>
/// Gets the name.
diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
index bc62ca4d5..fdba64c4a 100644
--- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
@@ -15,17 +16,6 @@ namespace MediaBrowser.LocalMetadata.Images
/// </summary>
public class EpisodeLocalImageProvider : ILocalImageProvider, IHasOrder
{
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EpisodeLocalImageProvider"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- public EpisodeLocalImageProvider(IFileSystem fileSystem)
- {
- _fileSystem = fileSystem;
- }
-
/// <inheritdoc />
public string Name => "Local Images";
@@ -49,14 +39,14 @@ namespace MediaBrowser.LocalMetadata.Images
var parentPathFiles = directoryService.GetFiles(parentPath);
- var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path);
+ var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
}
- private List<LocalImageInfo> GetFilesFromParentFolder(string filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
+ private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
{
- var thumbName = filenameWithoutExtension + "-thumb";
+ var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
var list = new List<LocalImageInfo>(1);
@@ -67,15 +57,15 @@ namespace MediaBrowser.LocalMetadata.Images
continue;
}
- if (BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
+ if (BaseItem.SupportedImageExtensions.Contains(i.Extension.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
- var currentNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(i);
+ var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i.FullName.AsSpan());
- if (string.Equals(filenameWithoutExtension, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+ if (filenameWithoutExtension.Equals(currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary });
}
- else if (string.Equals(thumbName, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+ else if (currentNameWithoutExtension.Equals(thumbName, StringComparison.OrdinalIgnoreCase))
{
list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary });
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index e8aeabf9d..a0ec3bd90 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index ef9943722..e86e518be 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -9,9 +9,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo
{
public class BdInfoDirectoryInfo : IDirectoryInfo
{
- private readonly IFileSystem _fileSystem = null;
+ private readonly IFileSystem _fileSystem;
- private readonly FileSystemMetadata _impl = null;
+ private readonly FileSystemMetadata _impl;
public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
{
@@ -29,7 +29,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
public string FullName => _impl.FullName;
- public IDirectoryInfo Parent
+ public IDirectoryInfo? Parent
{
get
{
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index 0a8af8e9c..41143c259 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -7,7 +7,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
{
public class BdInfoFileInfo : BDInfo.IO.IFileInfo
{
- private FileSystemMetadata _impl = null;
+ private FileSystemMetadata _impl;
public BdInfoFileInfo(FileSystemMetadata impl)
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 9e2417603..f782e65bd 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -121,11 +121,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
// When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions
public static Version MinVersion { get; } = new Version(4, 0);
- public static Version MaxVersion { get; } = null;
+ public static Version? MaxVersion { get; } = null;
public bool ValidateVersion()
{
- string output = null;
+ string output;
try
{
output = GetProcessOutput(_encoderPath, "-version");
@@ -133,6 +133,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
catch (Exception ex)
{
_logger.LogError(ex, "Error validating encoder");
+ return false;
}
if (string.IsNullOrWhiteSpace(output))
@@ -207,7 +208,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="output">The output from "ffmpeg -version".</param>
/// <returns>The FFmpeg version.</returns>
- internal Version GetFFmpegVersion(string output)
+ internal Version? GetFFmpegVersion(string output)
{
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
@@ -275,7 +276,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private IEnumerable<string> GetHwaccelTypes()
{
- string output = null;
+ string? output = null;
try
{
output = GetProcessOutput(_encoderPath, "-hwaccels");
@@ -303,7 +304,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
- string output = null;
+ string output;
try
{
output = GetProcessOutput(_encoderPath, "-h filter=" + filter);
@@ -311,6 +312,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
catch (Exception ex)
{
_logger.LogError(ex, "Error detecting the given filter");
+ return false;
}
if (output.Contains("Filter " + filter, StringComparison.Ordinal))
@@ -331,7 +333,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private IEnumerable<string> GetCodecs(Codec codec)
{
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
- string output = null;
+ string output;
try
{
output = GetProcessOutput(_encoderPath, "-" + codecstr);
@@ -339,6 +341,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
catch (Exception ex)
{
_logger.LogError(ex, "Error detecting available {Codec}", codecstr);
+ return Enumerable.Empty<string>();
}
if (string.IsNullOrWhiteSpace(output))
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 62c0c0bb1..3af618af8 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
@@ -366,7 +367,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
{
var prefix = "file";
- if (mediaSource.VideoType == VideoType.BluRay)
+ if (mediaSource.VideoType == VideoType.BluRay
+ || mediaSource.IsoType == IsoType.BluRay)
{
prefix = "bluray";
}
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 39fb0b47c..7733e715f 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -10,6 +10,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index da37687e8..1fa90bb21 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Globalization;
diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
index 0e319c1a8..d4d153b08 100644
--- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
+++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
index de062d06b..a1cef7a9f 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
index 8af122ef9..d50da37b8 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index 7b7744163..c9c8c34c2 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 884ec0a29..bbff5daca 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
@@ -123,19 +124,67 @@ namespace MediaBrowser.MediaEncoding.Probing
{
info.Name = title;
}
+ else
+ {
+ title = FFProbeHelpers.GetDictionaryValue(tags, "title-eng");
+ if (!string.IsNullOrWhiteSpace(title))
+ {
+ info.Name = title;
+ }
+ }
+
+ var titleSort = FFProbeHelpers.GetDictionaryValue(tags, "titlesort");
+ if (!string.IsNullOrWhiteSpace(titleSort))
+ {
+ info.ForcedSortName = titleSort;
+ }
info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort");
info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number");
info.ShowName = FFProbeHelpers.GetDictionaryValue(tags, "show_name");
info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
- // Several different forms of retaildate
- info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
+ // Several different forms of retail/premiere date
+ info.PremiereDate =
+ FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
+ // Set common metadata for music (audio) and music videos (video)
+ info.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
+
+ var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
+
+ if (!string.IsNullOrWhiteSpace(artists))
+ {
+ info.Artists = SplitArtists(artists, new[] { '/', ';' }, false)
+ .DistinctNames()
+ .ToArray();
+ }
+ else
+ {
+ var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
+ if (string.IsNullOrWhiteSpace(artist))
+ {
+ info.Artists = Array.Empty<string>();
+ }
+ else
+ {
+ info.Artists = SplitArtists(artist, _nameDelimiters, true)
+ .DistinctNames()
+ .ToArray();
+ }
+ }
+
+ // If we don't have a ProductionYear try and get it from PremiereDate
+ if (!info.ProductionYear.HasValue && info.PremiereDate.HasValue)
+ {
+ info.ProductionYear = info.PremiereDate.Value.Year;
+ }
+
+ // Set mediaType-specific metadata
if (isAudio)
{
SetAudioRuntimeTicks(data, info);
@@ -1078,13 +1127,13 @@ namespace MediaBrowser.MediaEncoding.Probing
private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags)
{
- var peoples = new List<BaseItemPerson>();
+ var people = new List<BaseItemPerson>();
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
if (!string.IsNullOrWhiteSpace(composer))
{
foreach (var person in Split(composer, false))
{
- peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
}
}
@@ -1093,7 +1142,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(conductor, false))
{
- peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
}
}
@@ -1102,46 +1151,21 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(lyricist, false))
{
- peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
}
}
// Check for writer some music is tagged that way as alternative to composer/lyricist
var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer");
-
if (!string.IsNullOrWhiteSpace(writer))
{
foreach (var person in Split(writer, false))
{
- peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
}
}
- audio.People = peoples.ToArray();
- audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
-
- var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
-
- if (!string.IsNullOrWhiteSpace(artists))
- {
- audio.Artists = SplitArtists(artists, new[] { '/', ';' }, false)
- .DistinctNames()
- .ToArray();
- }
- else
- {
- var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
- if (string.IsNullOrWhiteSpace(artist))
- {
- audio.Artists = Array.Empty<string>();
- }
- else
- {
- audio.Artists = SplitArtists(artist, _nameDelimiters, true)
- .DistinctNames()
- .ToArray();
- }
- }
+ audio.People = people.ToArray();
var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
if (string.IsNullOrWhiteSpace(albumArtist))
@@ -1176,12 +1200,6 @@ namespace MediaBrowser.MediaEncoding.Probing
// Disc number
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
- // If we don't have a ProductionYear try and get it from PremiereDate
- if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
- {
- audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
- }
-
// There's several values in tags may or may not be present
FetchStudios(audio, tags, "organization");
FetchStudios(audio, tags, "ensemble");
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
index 8219aa7b4..08ee5c72e 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.SubtitleFormats;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
deleted file mode 100644
index dca5c1e8a..000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-#nullable enable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- public static class ParserValues
- {
- public const string NewLine = "\r\n";
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
index 19fb951dc..78d54ca51 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.SubtitleFormats;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
index 36dc2e01f..17c2ae40e 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.SubtitleFormats;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index 82ec6ca21..639a34d99 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System.Globalization;
using System.IO;
using System.Linq;
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 39bec8da1..608ebf443 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -71,8 +72,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
- var reader = GetReader(inputFormat, true);
-
+ var reader = GetReader(inputFormat);
var trackInfo = reader.Parse(stream, cancellationToken);
FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
@@ -139,10 +139,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
.ConfigureAwait(false);
var inputFormat = subtitle.format;
- var writer = TryGetWriter(outputFormat);
// Return the original if we don't have any way of converting it
- if (writer == null)
+ if (!TryGetWriter(outputFormat, out var writer))
{
return subtitle.stream;
}
@@ -239,7 +238,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
.TrimStart('.');
- if (GetReader(currentFormat, false) == null)
+ if (TryGetReader(currentFormat, out _))
{
// Convert
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
@@ -257,37 +256,41 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true);
}
- private ISubtitleParser GetReader(string format, bool throwIfMissing)
+ private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
{
- if (string.IsNullOrEmpty(format))
- {
- throw new ArgumentNullException(nameof(format));
- }
-
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
{
- return new SrtParser(_logger);
+ value = new SrtParser(_logger);
+ return true;
}
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
{
- return new SsaParser(_logger);
+ value = new SsaParser(_logger);
+ return true;
}
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
{
- return new AssParser(_logger);
+ value = new AssParser(_logger);
+ return true;
}
- if (throwIfMissing)
+ value = null;
+ return false;
+ }
+
+ private ISubtitleParser GetReader(string format)
+ {
+ if (TryGetReader(format, out var reader))
{
- throw new ArgumentException("Unsupported format: " + format);
+ return reader;
}
- return null;
+ throw new ArgumentException("Unsupported format: " + format);
}
- private ISubtitleWriter TryGetWriter(string format)
+ private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
{
if (string.IsNullOrEmpty(format))
{
@@ -296,32 +299,35 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
- return new JsonWriter();
+ value = new JsonWriter();
+ return true;
}
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
{
- return new SrtWriter();
+ value = new SrtWriter();
+ return true;
}
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
{
- return new VttWriter();
+ value = new VttWriter();
+ return true;
}
if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
{
- return new TtmlWriter();
+ value = new TtmlWriter();
+ return true;
}
- return null;
+ value = null;
+ return false;
}
private ISubtitleWriter GetWriter(string format)
{
- var writer = TryGetWriter(format);
-
- if (writer != null)
+ if (TryGetWriter(format, out var writer))
{
return writer;
}
@@ -391,7 +397,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentNullException(nameof(outputPath));
}
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
@@ -549,7 +555,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentNullException(nameof(outputPath));
}
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
var processArgs = string.Format(
CultureInfo.InvariantCulture,
@@ -715,7 +721,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
{
- var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName;
+ var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty;
// UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal))
@@ -725,7 +731,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
charset = string.Empty;
}
- _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path);
+ _logger.LogDebug("charset {0} detected for {Path}", charset, path);
return charset;
}
diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs
index bffaa8594..2c9b5f005 100644
--- a/MediaBrowser.Model/Configuration/PathSubstitution.cs
+++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
namespace MediaBrowser.Model.Configuration
{
/// <summary>
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index e644c9ba7..c67f30d04 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -104,6 +104,19 @@ namespace MediaBrowser.Model.Entities
return "HDR";
}
+ // For some Dolby Vision files, no color transfer is provided, so check the codec
+
+ var codecTag = CodecTag;
+
+ if (string.Equals(codecTag, "dva1", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codecTag, "dvav", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
+ {
+ return "HDR";
+ }
+
return "SDR";
}
}
diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs
index 2d9a6c4db..77cbef00f 100644
--- a/MediaBrowser.Model/Extensions/StringHelper.cs
+++ b/MediaBrowser.Model/Extensions/StringHelper.cs
@@ -17,7 +17,8 @@ namespace MediaBrowser.Model.Extensions
return str;
}
- if (char.IsUpper(str[0]))
+ // We check IsLower instead of IsUpper because both return false for non-letters
+ if (!char.IsLower(str[0]))
{
return str;
}
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index e5c26430a..be4f1e16b 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -25,7 +24,7 @@ namespace MediaBrowser.Model.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
- string ResolveShortcut(string filename);
+ string? ResolveShortcut(string filename);
/// <summary>
/// Creates the shortcut.
@@ -160,7 +159,7 @@ namespace MediaBrowser.Model.IO
/// <returns>All found files.</returns>
IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false);
- IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive);
+ IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive);
/// <summary>
/// Gets the file system entries.
@@ -186,7 +185,7 @@ namespace MediaBrowser.Model.IO
/// <returns>IEnumerable&lt;System.String&gt;.</returns>
IEnumerable<string> GetFilePaths(string path, bool recursive = false);
- IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive);
+ IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive);
/// <summary>
/// Gets the file system entry paths.
diff --git a/MediaBrowser.Model/IO/IShortcutHandler.cs b/MediaBrowser.Model/IO/IShortcutHandler.cs
index 14d5c4b62..2c364a962 100644
--- a/MediaBrowser.Model/IO/IShortcutHandler.cs
+++ b/MediaBrowser.Model/IO/IShortcutHandler.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.IO
/// </summary>
/// <param name="shortcutPath">The shortcut path.</param>
/// <returns>System.String.</returns>
- string Resolve(string shortcutPath);
+ string? Resolve(string shortcutPath);
/// <summary>
/// Creates the specified shortcut path.
diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs
index 0e09db16e..f900da556 100644
--- a/MediaBrowser.Model/IO/IStreamHelper.cs
+++ b/MediaBrowser.Model/IO/IStreamHelper.cs
@@ -9,7 +9,7 @@ namespace MediaBrowser.Model.IO
{
public interface IStreamHelper
{
- Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken);
+ Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken);
Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken);
diff --git a/MediaBrowser.Model/MediaInfo/MediaInfo.cs b/MediaBrowser.Model/MediaInfo/MediaInfo.cs
index a268a4fa6..453aeb028 100644
--- a/MediaBrowser.Model/MediaInfo/MediaInfo.cs
+++ b/MediaBrowser.Model/MediaInfo/MediaInfo.cs
@@ -51,6 +51,8 @@ namespace MediaBrowser.Model.MediaInfo
public string ShowName { get; set; }
+ public string ForcedSortName { get; set; }
+
public int? IndexNumber { get; set; }
public int? ParentIndexNumber { get; set; }
diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
index b3db57b6d..d5c3a6aec 100644
--- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
+++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Model/Plugins/PluginInfo.cs b/MediaBrowser.Model/Plugins/PluginInfo.cs
index 25216610d..8eb90bdb0 100644
--- a/MediaBrowser.Model/Plugins/PluginInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
namespace MediaBrowser.Model.Plugins
diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
index 85c0aa204..f4d83c28b 100644
--- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
namespace MediaBrowser.Model.Plugins
{
/// <summary>
diff --git a/MediaBrowser.Model/Querying/EpisodeQuery.cs b/MediaBrowser.Model/Querying/EpisodeQuery.cs
deleted file mode 100644
index 56a7f3320..000000000
--- a/MediaBrowser.Model/Querying/EpisodeQuery.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Querying
-{
- public class EpisodeQuery
- {
- public EpisodeQuery()
- {
- Fields = Array.Empty<ItemFields>();
- }
-
- /// <summary>
- /// Gets or sets the user identifier.
- /// </summary>
- /// <value>The user identifier.</value>
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the season identifier.
- /// </summary>
- /// <value>The season identifier.</value>
- public string SeasonId { get; set; }
-
- /// <summary>
- /// Gets or sets the series identifier.
- /// </summary>
- /// <value>The series identifier.</value>
- public string SeriesId { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is missing.
- /// </summary>
- /// <value><c>null</c> if [is missing] contains no value, <c>true</c> if [is missing]; otherwise, <c>false</c>.</value>
- public bool? IsMissing { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is virtual unaired.
- /// </summary>
- /// <value><c>null</c> if [is virtual unaired] contains no value, <c>true</c> if [is virtual unaired]; otherwise, <c>false</c>.</value>
- public bool? IsVirtualUnaired { get; set; }
-
- /// <summary>
- /// Gets or sets the season number.
- /// </summary>
- /// <value>The season number.</value>
- public int? SeasonNumber { get; set; }
-
- /// <summary>
- /// Gets or sets the fields.
- /// </summary>
- /// <value>The fields.</value>
- public ItemFields[] Fields { get; set; }
-
- /// <summary>
- /// Gets or sets the start index.
- /// </summary>
- /// <value>The start index.</value>
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the limit.
- /// </summary>
- /// <value>The limit.</value>
- public int? Limit { get; set; }
-
- /// <summary>
- /// Gets or sets the start item identifier.
- /// </summary>
- /// <value>The start item identifier.</value>
- public string StartItemId { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs
deleted file mode 100644
index b800f5de5..000000000
--- a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Querying
-{
- public class MovieRecommendationQuery
- {
- public MovieRecommendationQuery()
- {
- ItemLimit = 10;
- CategoryLimit = 6;
- Fields = Array.Empty<ItemFields>();
- }
-
- /// <summary>
- /// Gets or sets the user identifier.
- /// </summary>
- /// <value>The user identifier.</value>
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the parent identifier.
- /// </summary>
- /// <value>The parent identifier.</value>
- public string ParentId { get; set; }
-
- /// <summary>
- /// Gets or sets the item limit.
- /// </summary>
- /// <value>The item limit.</value>
- public int ItemLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the category limit.
- /// </summary>
- /// <value>The category limit.</value>
- public int CategoryLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the fields.
- /// </summary>
- /// <value>The fields.</value>
- public ItemFields[] Fields { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 0555afc00..fa8aa829d 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -13,6 +13,7 @@ namespace MediaBrowser.Model.Querying
EnableImageTypes = Array.Empty<ImageType>();
EnableTotalRecordCount = true;
DisableFirstEpisode = false;
+ NextUpDateCutoff = DateTime.MinValue;
}
/// <summary>
@@ -75,5 +76,10 @@ namespace MediaBrowser.Model.Querying
/// Gets or sets a value indicating whether do disable sending first episode as next up.
/// </summary>
public bool DisableFirstEpisode { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
+ /// </summary>
+ public DateTime NextUpDateCutoff { get; set; }
}
}
diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
deleted file mode 100644
index 2cf0f0d5f..000000000
--- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Model.Querying
-{
- public class UpcomingEpisodesQuery
- {
- public UpcomingEpisodesQuery()
- {
- EnableImageTypes = Array.Empty<ImageType>();
- }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the parent identifier.
- /// </summary>
- /// <value>The parent identifier.</value>
- public string ParentId { get; set; }
-
- /// <summary>
- /// Gets or sets the start index. Use for paging.
- /// </summary>
- /// <value>The start index.</value>
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum number of items to return.
- /// </summary>
- /// <value>The limit.</value>
- public int? Limit { get; set; }
-
- /// <summary>
- /// Gets or sets the fields to return within the items, in addition to basic information.
- /// </summary>
- /// <value>The fields.</value>
- public ItemFields[] Fields { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [enable images].
- /// </summary>
- /// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value>
- public bool? EnableImages { get; set; }
-
- /// <summary>
- /// Gets or sets the image type limit.
- /// </summary>
- /// <value>The image type limit.</value>
- public int? ImageTypeLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the enable image types.
- /// </summary>
- /// <value>The enable image types.</value>
- public ImageType[] EnableImageTypes { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Sync/SyncCategory.cs b/MediaBrowser.Model/Sync/SyncCategory.cs
deleted file mode 100644
index 1248c2f73..000000000
--- a/MediaBrowser.Model/Sync/SyncCategory.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Sync
-{
- public enum SyncCategory
- {
- /// <summary>
- /// The latest.
- /// </summary>
- Latest = 0,
-
- /// <summary>
- /// The next up.
- /// </summary>
- NextUp = 1,
-
- /// <summary>
- /// The resume.
- /// </summary>
- Resume = 2
- }
-}
diff --git a/MediaBrowser.Model/Sync/SyncJob.cs b/MediaBrowser.Model/Sync/SyncJob.cs
deleted file mode 100644
index 3e396e5d1..000000000
--- a/MediaBrowser.Model/Sync/SyncJob.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Sync
-{
- public class SyncJob
- {
- public SyncJob()
- {
- RequestedItemIds = Array.Empty<Guid>();
- }
-
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the device identifier.
- /// </summary>
- /// <value>The device identifier.</value>
- public string TargetId { get; set; }
-
- /// <summary>
- /// Gets or sets the name of the target.
- /// </summary>
- /// <value>The name of the target.</value>
- public string TargetName { get; set; }
-
- /// <summary>
- /// Gets or sets the quality.
- /// </summary>
- /// <value>The quality.</value>
- public string Quality { get; set; }
-
- /// <summary>
- /// Gets or sets the bitrate.
- /// </summary>
- /// <value>The bitrate.</value>
- public int? Bitrate { get; set; }
-
- /// <summary>
- /// Gets or sets the profile.
- /// </summary>
- /// <value>The profile.</value>
- public string Profile { get; set; }
-
- /// <summary>
- /// Gets or sets the category.
- /// </summary>
- /// <value>The category.</value>
- public SyncCategory? Category { get; set; }
-
- /// <summary>
- /// Gets or sets the parent identifier.
- /// </summary>
- /// <value>The parent identifier.</value>
- public string ParentId { get; set; }
-
- /// <summary>
- /// Gets or sets the current progress.
- /// </summary>
- /// <value>The current progress.</value>
- public double? Progress { get; set; }
-
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the status.
- /// </summary>
- /// <value>The status.</value>
- public SyncJobStatus Status { get; set; }
-
- /// <summary>
- /// Gets or sets the user identifier.
- /// </summary>
- /// <value>The user identifier.</value>
- public string UserId { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [unwatched only].
- /// </summary>
- /// <value><c>true</c> if [unwatched only]; otherwise, <c>false</c>.</value>
- public bool UnwatchedOnly { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [synchronize new content].
- /// </summary>
- /// <value><c>true</c> if [synchronize new content]; otherwise, <c>false</c>.</value>
- public bool SyncNewContent { get; set; }
-
- /// <summary>
- /// Gets or sets the item limit.
- /// </summary>
- /// <value>The item limit.</value>
- public int? ItemLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the requested item ids.
- /// </summary>
- /// <value>The requested item ids.</value>
- public Guid[] RequestedItemIds { get; set; }
-
- /// <summary>
- /// Gets or sets the date created.
- /// </summary>
- /// <value>The date created.</value>
- public DateTime DateCreated { get; set; }
-
- /// <summary>
- /// Gets or sets the date last modified.
- /// </summary>
- /// <value>The date last modified.</value>
- public DateTime DateLastModified { get; set; }
-
- /// <summary>
- /// Gets or sets the item count.
- /// </summary>
- /// <value>The item count.</value>
- public int ItemCount { get; set; }
-
- public string ParentName { get; set; }
-
- public string PrimaryImageItemId { get; set; }
-
- public string PrimaryImageTag { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Sync/SyncJobStatus.cs b/MediaBrowser.Model/Sync/SyncJobStatus.cs
deleted file mode 100644
index 226a47d4c..000000000
--- a/MediaBrowser.Model/Sync/SyncJobStatus.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Sync
-{
- public enum SyncJobStatus
- {
- Queued = 0,
- Converting = 1,
- ReadyToTransfer = 2,
- Transferring = 3,
- Completed = 4,
- CompletedWithError = 5,
- Failed = 6
- }
-}
diff --git a/MediaBrowser.Model/Sync/SyncTarget.cs b/MediaBrowser.Model/Sync/SyncTarget.cs
deleted file mode 100644
index 9e6bbbc00..000000000
--- a/MediaBrowser.Model/Sync/SyncTarget.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Sync
-{
- public class SyncTarget
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
index cbd60cca1..999db9605 100644
--- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs
+++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
@@ -11,12 +11,12 @@ namespace MediaBrowser.Model.Tasks
/// <summary>
/// Fires when the trigger condition is satisfied and the task should run.
/// </summary>
- event EventHandler<EventArgs> Triggered;
+ event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the options of this task.
+ /// Gets the options of this task.
/// </summary>
- TaskOptions TaskOptions { get; set; }
+ TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs
index 7a82685f0..aeaaa8b35 100644
--- a/MediaBrowser.Model/Updates/PackageInfo.cs
+++ b/MediaBrowser.Model/Updates/PackageInfo.cs
@@ -1,4 +1,3 @@
-#nullable enable
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -16,7 +15,6 @@ namespace MediaBrowser.Model.Updates
public PackageInfo()
{
Versions = Array.Empty<VersionInfo>();
- Id = string.Empty;
Category = string.Empty;
Name = string.Empty;
Overview = string.Empty;
@@ -65,7 +63,7 @@ namespace MediaBrowser.Model.Updates
/// </summary>
/// <value>The name.</value>
[JsonPropertyName("guid")]
- public string Id { get; set; }
+ public Guid Id { get; set; }
/// <summary>
/// Gets or sets the versions.
diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs
index 209092265..03a540dde 100644
--- a/MediaBrowser.Model/Updates/VersionInfo.cs
+++ b/MediaBrowser.Model/Updates/VersionInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System.Text.Json.Serialization;
using SysVersion = System.Version;
diff --git a/MediaBrowser.Model/Users/UserAction.cs b/MediaBrowser.Model/Users/UserAction.cs
deleted file mode 100644
index 7646db4a8..000000000
--- a/MediaBrowser.Model/Users/UserAction.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Users
-{
- public class UserAction
- {
- public string Id { get; set; }
-
- public string ServerId { get; set; }
-
- public Guid UserId { get; set; }
-
- public Guid ItemId { get; set; }
-
- public UserActionType Type { get; set; }
-
- public DateTime Date { get; set; }
-
- public long? PositionTicks { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 4471a25b2..966a3d822 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Manager
/// <summary>
/// Image types that are only one per item.
/// </summary>
- private readonly ImageType[] _singularImages =
+ private static readonly ImageType[] _singularImages =
{
ImageType.Primary,
ImageType.Art,
@@ -208,9 +208,14 @@ namespace MediaBrowser.Providers.Manager
/// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns>
private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit, int screenshotLimit)
{
- if (_singularImages.Any(i => images.Contains(i) && !HasImage(item, i) && savedOptions.GetLimit(i) > 0))
+ // Using .Any causes the creation of a DisplayClass aka. variable capture
+ for (var i = 0; i < _singularImages.Length; i++)
{
- return false;
+ var type = _singularImages[i];
+ if (images.Contains(type) && !HasImage(item, type) && savedOptions.GetLimit(type) > 0)
+ {
+ return false;
+ }
}
if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit)
@@ -329,7 +334,7 @@ namespace MediaBrowser.Providers.Manager
var deleted = false;
var deletedImages = new List<ItemImageInfo>();
- foreach (var image in item.GetImages(type).ToList())
+ foreach (var image in item.GetImages(type))
{
if (!image.IsLocalFile)
{
@@ -359,9 +364,10 @@ namespace MediaBrowser.Providers.Manager
{
var changed = false;
- foreach (var type in _singularImages)
+ for (var i = 0; i < _singularImages.Length; i++)
{
- var image = images.FirstOrDefault(i => i.Type == type);
+ var type = _singularImages[i];
+ var image = GetFirstLocalImageInfoByType(images, type);
if (image != null)
{
@@ -423,15 +429,29 @@ namespace MediaBrowser.Providers.Manager
return changed;
}
+ private static LocalImageInfo GetFirstLocalImageInfoByType(IReadOnlyList<LocalImageInfo> images, ImageType type)
+ {
+ var len = images.Count;
+ for (var i = 0; i < len; i++)
+ {
+ var image = images[i];
+ if (image.Type == type)
+ {
+ return image;
+ }
+ }
+
+ return null;
+ }
+
private bool UpdateMultiImages(BaseItem item, List<LocalImageInfo> images, ImageType type)
{
var changed = false;
- var newImages = images.Where(i => i.Type == type).ToList();
-
- var newImageFileInfos = newImages
- .Select(i => i.FileInfo)
- .ToList();
+ var newImageFileInfos = images
+ .FindAll(i => i.Type == type)
+ .Select(i => i.FileInfo)
+ .ToList();
if (item.AddImages(type, newImageFileInfos))
{
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 401c7e99f..827cb69b9 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -28,8 +28,11 @@ namespace MediaBrowser.Providers.Manager
ProviderManager = providerManager;
FileSystem = fileSystem;
LibraryManager = libraryManager;
+ ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
}
+ protected ItemImageProvider ImageProvider { get; }
+
protected IServerConfigurationManager ServerConfigurationManager { get; }
protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; }
@@ -88,7 +91,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
var localImagesFailed = false;
var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList();
@@ -97,7 +99,7 @@ namespace MediaBrowser.Providers.Manager
try
{
// Always validate images and check for new locally stored ones.
- if (itemImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService))
+ if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService))
{
updateType |= ItemUpdateType.ImageUpdate;
}
@@ -143,7 +145,7 @@ namespace MediaBrowser.Providers.Manager
// await FindIdentities(id, cancellationToken).ConfigureAwait(false);
id.IsAutomated = refreshOptions.IsAutomated;
- var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false);
+ var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
@@ -160,7 +162,7 @@ namespace MediaBrowser.Providers.Manager
if (providers.Count > 0)
{
- var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
+ var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
@@ -211,9 +213,23 @@ namespace MediaBrowser.Providers.Manager
private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result)
{
- lookupInfo.ProviderIds = result.ProviderIds;
- lookupInfo.Name = result.Name;
- lookupInfo.Year = result.ProductionYear;
+ // Episode and Season do not support Identify, so the search results are the Series'
+ switch (lookupInfo)
+ {
+ case EpisodeInfo episodeInfo:
+ episodeInfo.SeriesProviderIds = result.ProviderIds;
+ episodeInfo.ProviderIds.Clear();
+ break;
+ case SeasonInfo seasonInfo:
+ seasonInfo.SeriesProviderIds = result.ProviderIds;
+ seasonInfo.ProviderIds.Clear();
+ break;
+ default:
+ lookupInfo.ProviderIds = result.ProviderIds;
+ lookupInfo.Name = result.Name;
+ lookupInfo.Year = result.ProductionYear;
+ break;
+ }
}
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
@@ -563,7 +579,7 @@ namespace MediaBrowser.Providers.Manager
protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options)
{
// Get providers to refresh
- var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)).ToList();
+ var providers = allImageProviders.Where(i => !(i is ILocalImageProvider));
var dateLastImageRefresh = item.DateLastRefreshed;
@@ -575,15 +591,13 @@ namespace MediaBrowser.Providers.Manager
providers = providers
.Where(i =>
{
- var hasFileChangeMonitor = i as IHasItemChangeMonitor;
- if (hasFileChangeMonitor != null)
+ if (i is IHasItemChangeMonitor hasFileChangeMonitor)
{
return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
}
return false;
- })
- .ToList();
+ });
}
return providers;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index dd497845d..2dfaa372c 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1111,7 +1111,7 @@ namespace MediaBrowser.Providers.Manager
await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
break;
case Folder folder:
- await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
+ await folder.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
}
}
@@ -1122,7 +1122,7 @@ namespace MediaBrowser.Providers.Manager
{
await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
- await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
+ await child.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -1144,7 +1144,7 @@ namespace MediaBrowser.Providers.Manager
.Select(i => i.MusicArtist)
.Where(i => i != null);
- var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true));
+ var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), options, true, cancellationToken));
await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs
index 5621d2b86..e5aa64b28 100644
--- a/MediaBrowser.Providers/Manager/ProviderUtils.cs
+++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs
@@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.Manager
}
}
- if (replaceData || !target.CommunityRating.HasValue || (source.CommunityRating.HasValue && string.Equals(sourceResult.Provider, "The Open Movie Database", StringComparison.OrdinalIgnoreCase)))
+ if (replaceData || !target.CommunityRating.HasValue)
{
target.CommunityRating = source.CommunityRating;
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
index 945463666..cf271e7db 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
@@ -111,6 +111,11 @@ namespace MediaBrowser.Providers.MediaInfo
audio.Name = data.Name;
}
+ if (!string.IsNullOrEmpty(data.ForcedSortName))
+ {
+ audio.ForcedSortName = data.ForcedSortName;
+ }
+
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List<PersonInfo>();
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index f049cc81f..12e1fbea5 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -147,7 +147,8 @@ namespace MediaBrowser.Providers.MediaInfo
{
Path = path,
Protocol = protocol,
- VideoType = item.VideoType
+ VideoType = item.VideoType,
+ IsoType = item.IsoType
}
},
cancellationToken);
@@ -391,6 +392,12 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
+ if (video is MusicVideo musicVideo)
+ {
+ musicVideo.Album = data.Album;
+ musicVideo.Artists = data.Artists;
+ }
+
if (data.ProductionYear.HasValue)
{
if (!video.ProductionYear.HasValue || isFullRefresh)
@@ -433,6 +440,11 @@ namespace MediaBrowser.Providers.MediaInfo
video.Name = data.Name;
}
}
+
+ if (!string.IsNullOrWhiteSpace(data.ForcedSortName))
+ {
+ video.ForcedSortName = data.ForcedSortName;
+ }
}
// If we don't have a ProductionYear try and get it from PremiereDate
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index b086ef07b..3cd7ec772 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -55,38 +54,35 @@ namespace MediaBrowser.Providers.MediaInfo
return streams;
}
- public List<string> GetExternalSubtitleFiles(
+ public IEnumerable<string> GetExternalSubtitleFiles(
Video video,
IDirectoryService directoryService,
bool clearCache)
{
- var list = new List<string>();
-
if (!video.IsFileProtocol)
{
- return list;
+ yield break;
}
var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache);
foreach (var stream in streams)
{
- list.Add(stream.Path);
+ yield return stream.Path;
}
-
- return list;
}
public void AddExternalSubtitleStreams(
List<MediaStream> streams,
string videoPath,
int startIndex,
- string[] files)
+ IReadOnlyList<string> files)
{
var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath);
- foreach (var fullName in files)
+ for (var i = 0; i < files.Count; i++)
{
+ var fullName = files[i];
var extension = Path.GetExtension(fullName.AsSpan());
if (!IsSubtitleExtension(extension))
{
@@ -135,15 +131,12 @@ namespace MediaBrowser.Providers.MediaInfo
break;
}
- var language = languageSpan.ToString();
// Try to translate to three character code
// Be flexible and check against both the full and three character versions
+ var language = languageSpan.ToString();
var culture = _localization.FindLanguageInfo(language);
- if (culture != null)
- {
- language = culture.ThreeLetterISOLanguageName;
- }
+ language = culture == null ? language : culture.ThreeLetterISOLanguageName;
mediaStream = new MediaStream
{
@@ -194,7 +187,7 @@ namespace MediaBrowser.Providers.MediaInfo
IDirectoryService directoryService,
bool clearCache)
{
- var files = directoryService.GetFilePaths(folder, clearCache).OrderBy(i => i).ToArray();
+ var files = directoryService.GetFilePaths(folder, clearCache, true);
AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
}
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index f448ad38b..e49c0e77b 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -42,7 +43,7 @@ namespace Rssdp.Infrastructure
private HttpResponseParser _ResponseParser;
private readonly ILogger _logger;
private ISocketFactory _SocketFactory;
- private readonly INetworkManager _networkManager;
+ private readonly INetworkManager _networkManager;
private int _LocalPort;
private int _MulticastTtl;
@@ -68,7 +69,7 @@ namespace Rssdp.Infrastructure
INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
{
-
+
}
/// <summary>
@@ -358,7 +359,7 @@ namespace Rssdp.Infrastructure
{
// Not support IPv6 right now
continue;
- }
+ }
try
{
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index 092364500..4c426b6d5 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index eef272d5b..7ed6d52bc 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index 154481d64..b46cceaa4 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 171ebe372..a0e23557a 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
index a0a5f6923..af0f55f8e 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
index ae59802cd..ba004bb6a 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
index 236b35ce5..0d1114c01 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index 2e80d4a6e..b57dc53f5 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 852a6b553..3783dfacf 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index bffeb7307..663a7af9e 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index e90da4636..83eb24e42 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index aae262ee6..1187f37b9 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index c847b1621..8b2361f0b 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/ef13f9da-46dc-4de9-a05e-5a4c20574189/be95913ebf1fb6c66833ca40060d3f65/dotnet-sdk-5.0.203-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/fuzz/.gitignore b/fuzz/.gitignore
new file mode 100644
index 000000000..652de0a45
--- /dev/null
+++ b/fuzz/.gitignore
@@ -0,0 +1 @@
+Findings
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
new file mode 100644
index 000000000..791cb140d
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="Emby.Server.Implementations">
+ <HintPath>Emby.Server.Implementations.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="SharpFuzz" Version="1.6.2" />
+ </ItemGroup>
+
+</Project>
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
new file mode 100644
index 000000000..a4a6f5f54
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
@@ -0,0 +1,32 @@
+using System;
+using Emby.Server.Implementations.Library;
+using SharpFuzz;
+
+namespace Emby.Server.Implementations.Fuzz
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ switch (args[0])
+ {
+ case "PathExtensions.TryReplaceSubPath": Run(PathExtensions_TryReplaceSubPath); return;
+ default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}");
+ }
+ }
+
+ private static void Run(Action<string> action) => Fuzzer.OutOfProcess.Run(action);
+
+ private static void PathExtensions_TryReplaceSubPath(string data)
+ {
+ // Stupid, but it worked
+ var parts = data.Split(':');
+ if (parts.Length != 3)
+ {
+ return;
+ }
+
+ _ = PathExtensions.TryReplaceSubPath(parts[0], parts[1], parts[2], out _);
+ }
+ }
+}
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt
new file mode 100644
index 000000000..aacf973d6
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt
@@ -0,0 +1 @@
+/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt/:/home/bond/dev/jellyfin/:/srv/jellyfin/
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
new file mode 100755
index 000000000..244f73402
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+set -e
+
+dotnet build -c Release ../../Emby.Server.Implementations/Emby.Server.Implementations.csproj --output bin
+sharpfuzz bin/Emby.Server.Implementations.dll
+cp bin/Emby.Server.Implementations.dll .
+
+dotnet build
+mkdir -p Findings
+AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net5.0/Emby.Server.Implementations.Fuzz.dll "$1"
diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
new file mode 100644
index 000000000..6fcfbae0e
--- /dev/null
+++ b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="Jellyfin.Server">
+ <HintPath>jellyfin.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="SharpFuzz" Version="1.6.2" />
+ </ItemGroup>
+
+</Project>
diff --git a/fuzz/Jellyfin.Server.Fuzz/Program.cs b/fuzz/Jellyfin.Server.Fuzz/Program.cs
new file mode 100644
index 000000000..e47286c13
--- /dev/null
+++ b/fuzz/Jellyfin.Server.Fuzz/Program.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Server.Middleware;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+using SharpFuzz;
+
+namespace Emby.Server.Implementations.Fuzz
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ switch (args[0])
+ {
+ case "UrlDecodeQueryFeature": Run(UrlDecodeQueryFeature); return;
+ default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}");
+ }
+ }
+
+ private static void Run(Action<string> action) => Fuzzer.OutOfProcess.Run(action);
+
+ private static void UrlDecodeQueryFeature(string data)
+ {
+ var dict = new Dictionary<string, StringValues>
+ {
+ { data, StringValues.Empty }
+ };
+ _ = new UrlDecodeQueryFeature(new QueryFeature(new QueryCollection(dict)));
+ }
+ }
+}
diff --git a/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt b/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
new file mode 100644
index 000000000..73f356b93
--- /dev/null
+++ b/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt
@@ -0,0 +1 @@
+a%3D1%26b%3D2%26c%3D3
diff --git a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh
new file mode 100755
index 000000000..ad81e2c35
--- /dev/null
+++ b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+set -e
+
+dotnet build -c Release ../../Jellyfin.Server/Jellyfin.Server.csproj --output bin
+sharpfuzz bin/jellyfin.dll
+cp bin/jellyfin.dll .
+
+dotnet build
+mkdir -p Findings
+AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net5.0/Jellyfin.Server.Fuzz.dll "$1"
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 1a9f2bf96..44bc34369 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
+ <!-- disable warning CA1040: Avoid empty interfaces -->
+ <Rule Id="CA1040" Action="Info" />
+
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 839cfb280..d4ea91872 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -18,9 +18,9 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 8018b2966..546b2487e 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -15,10 +15,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
index 9ded01f2b..7629d9912 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
@@ -1,34 +1,45 @@
-using System.Text.Json;
+using System.Globalization;
+using System.Text.Json;
+using FsCheck;
+using FsCheck.Xunit;
using MediaBrowser.Common.Json.Converters;
using Xunit;
namespace Jellyfin.Common.Tests.Json
{
- public static class JsonBoolNumberTests
+ public class JsonBoolNumberTests
{
+ private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
+ {
+ Converters =
+ {
+ new JsonBoolNumberConverter()
+ }
+ };
+
[Theory]
[InlineData("1", true)]
[InlineData("0", false)]
[InlineData("2", true)]
[InlineData("true", true)]
[InlineData("false", false)]
- public static void Deserialize_Number_Valid_Success(string input, bool? output)
+ public void Deserialize_Number_Valid_Success(string input, bool? output)
{
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonBoolNumberConverter());
- var value = JsonSerializer.Deserialize<bool>(input, options);
+ var value = JsonSerializer.Deserialize<bool>(input, _jsonOptions);
Assert.Equal(value, output);
}
[Theory]
[InlineData(true, "true")]
[InlineData(false, "false")]
- public static void Serialize_Bool_Success(bool input, string output)
+ public void Serialize_Bool_Success(bool input, string output)
{
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonBoolNumberConverter());
- var value = JsonSerializer.Serialize(input, options);
+ var value = JsonSerializer.Serialize(input, _jsonOptions);
Assert.Equal(value, output);
}
+
+ [Property]
+ public Property Deserialize_NonZeroInt_True(NonZeroInt input)
+ => JsonSerializer.Deserialize<bool>(input.ToString(), _jsonOptions).ToProperty();
}
-} \ No newline at end of file
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
index fd77694b3..2b23c6705 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
@@ -6,14 +6,13 @@ namespace Jellyfin.Common.Tests.Json
{
public class JsonStringConverterTests
{
- private readonly JsonSerializerOptions _jsonSerializerOptions
- = new ()
+ private readonly JsonSerializerOptions _jsonSerializerOptions = new ()
+ {
+ Converters =
{
- Converters =
- {
- new JsonStringConverter()
- }
- };
+ new JsonStringConverter()
+ }
+ };
[Theory]
[InlineData("\"test\"", "test")]
@@ -36,4 +35,4 @@ namespace Jellyfin.Common.Tests.Json
Assert.Equal(deserialized, output);
}
}
-} \ No newline at end of file
+}
diff --git a/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs b/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs
new file mode 100644
index 000000000..576c0a49b
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs
@@ -0,0 +1,19 @@
+using System;
+using MediaBrowser.Controller.Extensions;
+using Xunit;
+
+namespace Jellyfin.Controller.Extensions.Tests
+{
+ public class StringExtensionsTests
+ {
+ [Theory]
+ [InlineData("", '_', 0)]
+ [InlineData("___", '_', 3)]
+ [InlineData("test\x00", '\x00', 1)]
+ [InlineData("Imdb=tt0119567|Tmdb=330|TmdbCollection=328", '|', 2)]
+ public void ReadOnlySpan_Count_Success(string str, char needle, int count)
+ {
+ Assert.Equal(count, str.AsSpan().Count(needle));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index ad1627698..9a8ddafa0 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index f7c21f072..1f6cd541c 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 8321d0255..6b828e113 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 69e2aa437..98fbb00d5 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Globalization;
using System.IO;
using System.Text.Json;
using MediaBrowser.Common.Json;
@@ -17,9 +19,9 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
[Fact]
public void GetMediaInfo_MetaData_Success()
{
- var bytes = File.ReadAllBytes("Test Data/Probing/some_matadata.json");
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_metadata.json");
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
- MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/some_matadata.mkv", MediaProtocol.File);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File);
Assert.Single(res.MediaStreams);
@@ -52,5 +54,22 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Empty(res.Chapters);
Assert.Equal("Just color bars", res.Overview);
}
+
+ [Fact]
+ public void GetMediaInfo_MusicVideo_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/music_video_metadata.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/music_video.mkv", MediaProtocol.File);
+
+ Assert.Equal("The Title", res.Name);
+ Assert.Equal("Title, The", res.ForcedSortName);
+ Assert.Single(res.Artists);
+ Assert.Equal("The Artist", res.Artists[0]);
+ Assert.Equal("Album", res.Album);
+ Assert.Equal(2021, res.ProductionYear);
+ Assert.True(res.PremiereDate.HasValue);
+ Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json
new file mode 100644
index 000000000..97d6600a4
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json
@@ -0,0 +1,111 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "High",
+ "codec_type": "video",
+ "codec_time_base": "1001/48000",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "width": 1920,
+ "height": 1080,
+ "coded_width": 1920,
+ "coded_height": 1088,
+ "closed_captions": 0,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "16:9",
+ "pix_fmt": "yuv420p",
+ "level": 42,
+ "chroma_location": "left",
+ "field_order": "progressive",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "r_frame_rate": "24000/1001",
+ "avg_frame_rate": "24000/1001",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "bits_per_raw_sample": "8",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_time_base": "1/48000",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "sample_fmt": "fltp",
+ "sample_rate": "48000",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ }
+ ],
+ "chapters": [
+ ],
+ "format": {
+ "filename": "music_video.mkv",
+ "nb_streams": 2,
+ "nb_programs": 0,
+ "format_name": "matroska,webm",
+ "format_long_name": "Matroska / WebM",
+ "start_time": "0.000000",
+ "duration": "180.000000",
+ "size": "500000000",
+ "bit_rate": "22222222",
+ "probe_score": 100,
+ "tags": {
+ "TITLE-eng": "The Title",
+ "TITLESORT": "Title, The",
+ "ARTIST": "The Artist",
+ "ARTISTSORT": "Artist, The",
+ "ALBUM": "Album",
+ "DATE_RELEASED": "2021-01-01"
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/some_matadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json
index 720fc5c8f..720fc5c8f 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/some_matadata.json
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json
diff --git a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
index 5864a0509..0a4e060df 100644
--- a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
+++ b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
@@ -1,3 +1,6 @@
+using System;
+using FsCheck;
+using FsCheck.Xunit;
using MediaBrowser.Model.Extensions;
using Xunit;
@@ -10,9 +13,20 @@ namespace Jellyfin.Model.Tests.Extensions
[InlineData("banana", "Banana")]
[InlineData("Banana", "Banana")]
[InlineData("ä", "Ä")]
+ [InlineData("\027", "\027")]
public void StringHelper_ValidArgs_Success(string input, string expectedResult)
{
Assert.Equal(expectedResult, StringHelper.FirstToUpper(input));
}
+
+ [Property]
+ public Property FirstToUpper_RandomArg_Correct(NonEmptyString input)
+ {
+ var result = StringHelper.FirstToUpper(input.Item);
+
+ // We check IsLower instead of IsUpper because both return false for non-letters
+ return (!char.IsLower(result[0])).Label("First char is uppercase")
+ .And(input.Item.Length == 1 || result[1..].Equals(input.Item[1..], StringComparison.Ordinal)).Label("Remaining chars are unmodified");
+ }
}
}
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index c5b51ef76..40c51e524 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -10,10 +10,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index ebb134fc3..e386cb8c1 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
index 950899d7e..b1141df47 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
@@ -58,7 +58,7 @@ namespace Jellyfin.Naming.Tests.Video
{
input = Path.GetFileName(input);
- var result = new VideoResolver(_namingOptions).CleanDateTime(input);
+ var result = VideoResolver.CleanDateTime(input, _namingOptions);
Assert.Equal(expectedName, result.Name, true);
Assert.Equal(expectedYear, result.Year);
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
index a720bdade..fb050cf5a 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
@@ -7,7 +7,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public sealed class CleanStringTests
{
- private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
+ private readonly NamingOptions _namingOptions = new NamingOptions();
[Theory]
[InlineData("Super movie 480p.mp4", "Super movie")]
@@ -26,7 +26,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
{
- Assert.True(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName));
+ Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
// TODO: compare spans when XUnit supports it
Assert.Equal(expectedName, newName.ToString());
}
@@ -41,7 +41,7 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Run lola run (lola rennt) (2009).mp4")]
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
{
- Assert.False(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName));
+ Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
Assert.True(newName.IsEmpty);
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index 2f173b0ce..f872f94f8 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -104,13 +104,6 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(rule, res.Rule);
}
- [Fact]
- public void TestFlagsParser()
- {
- var flags = new FlagParser(_videoOptions).GetFlags(string.Empty);
- Assert.Empty(flags);
- }
-
private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
{
return new ExtraResolver(videoOptions);
diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
index 69de96a47..1762b91b9 100644
--- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
@@ -22,8 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void Test3DName()
{
- var result =
- new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
+ var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions);
Assert.Equal("hsbs", result?.Format3D);
Assert.Equal("Oblivion", result?.Name);
@@ -58,15 +57,13 @@ namespace Jellyfin.Naming.Tests.Video
private void Test(string input, bool is3D, string? format3D)
{
- var parser = new Format3DParser(_namingOptions);
-
- var result = parser.Parse(input);
+ var result = Format3DParser.Parse(input, _namingOptions);
Assert.Equal(is3D, result.Is3D);
if (format3D == null)
{
- Assert.Null(result.Format3D);
+ Assert.Null(result?.Format3D);
}
else
{
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 6e803593e..d02f8ae92 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class MultiVersionTests
{
- private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions());
+ private readonly NamingOptions _namingOptions = new NamingOptions();
[Fact]
public void TestMultiEdition1()
@@ -22,11 +22,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
@@ -43,11 +45,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
@@ -63,11 +67,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -87,11 +93,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/M/Movie 7.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
@@ -113,11 +121,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Movie/Movie-8.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Mo/Movie 9.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(9, result.Count);
Assert.Empty(result[0].Extras);
@@ -163,11 +175,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Movie/Movie 5.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -188,11 +202,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man (2011).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -214,11 +230,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man[test].mkv",
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -243,11 +261,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man [test].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -266,11 +286,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man - C (2007).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -289,11 +311,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
@@ -314,11 +338,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man (2011).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -334,11 +360,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -354,11 +382,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -374,11 +404,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -394,11 +426,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -406,7 +440,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestEmptyList()
{
- var result = _videoListResolver.Resolve(new List<FileSystemMetadata>()).ToList();
+ var result = VideoListResolver.Resolve(new List<FileSystemMetadata>(), _namingOptions).ToList();
Assert.Empty(result);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
index 6e759c6d6..1d50df7a6 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
@@ -29,8 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestStubName()
{
- var result =
- new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
+ var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions);
Assert.Equal("Oblivion", result?.Name);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 08af76669..9e0776c3c 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class VideoListResolverTests
{
- private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions());
+ private readonly NamingOptions _namingOptions = new NamingOptions();
[Fact]
public void TestStackAndExtras()
@@ -40,11 +40,13 @@ namespace Jellyfin.Naming.Tests.Video
"WillyWonka-trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
@@ -67,11 +69,13 @@ namespace Jellyfin.Naming.Tests.Video
"300.nfo"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -85,11 +89,13 @@ namespace Jellyfin.Naming.Tests.Video
"300 trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -103,11 +109,13 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -122,11 +130,13 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer2.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video
"Looper.2012.bluray.720p.x264.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -162,11 +174,13 @@ namespace Jellyfin.Naming.Tests.Video
"My video 5.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
}
@@ -180,11 +194,13 @@ namespace Jellyfin.Naming.Tests.Video
@"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = true,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = true,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -199,11 +215,13 @@ namespace Jellyfin.Naming.Tests.Video
@"My movie #2.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = true,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = true,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -218,11 +236,13 @@ namespace Jellyfin.Naming.Tests.Video
@"No (2012) part1-trailer.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -237,11 +257,13 @@ namespace Jellyfin.Naming.Tests.Video
@"No (2012)-trailer.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -257,11 +279,13 @@ namespace Jellyfin.Naming.Tests.Video
@"trailer.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -277,11 +301,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -294,11 +320,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -311,11 +339,13 @@ namespace Jellyfin.Naming.Tests.Video
@"The Colony.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -329,11 +359,13 @@ namespace Jellyfin.Naming.Tests.Video
@"Four Sisters and a Wedding - B.avi"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -347,11 +379,13 @@ namespace Jellyfin.Naming.Tests.Video
@"Four Rooms - A.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -365,11 +399,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Server/Despicable Me/movie-trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -385,11 +421,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(4, result.Count);
}
@@ -403,11 +441,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Movies/Despicable Me/trailers/trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index c56046f03..ac5a7a21e 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class VideoResolverTests
{
- private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
+ private static NamingOptions _namingOptions = new NamingOptions();
public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
{
@@ -159,27 +159,27 @@ namespace Jellyfin.Naming.Tests.Video
[MemberData(nameof(ResolveFile_ValidFileNameTestData))]
public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult)
{
- var result = _videoResolver.ResolveFile(expectedResult.Path);
+ var result = VideoResolver.ResolveFile(expectedResult.Path, _namingOptions);
Assert.NotNull(result);
- Assert.Equal(result?.Path, expectedResult.Path);
- Assert.Equal(result?.Container, expectedResult.Container);
- Assert.Equal(result?.Name, expectedResult.Name);
- Assert.Equal(result?.Year, expectedResult.Year);
- Assert.Equal(result?.ExtraType, expectedResult.ExtraType);
- Assert.Equal(result?.Format3D, expectedResult.Format3D);
- Assert.Equal(result?.Is3D, expectedResult.Is3D);
- Assert.Equal(result?.IsStub, expectedResult.IsStub);
- Assert.Equal(result?.StubType, expectedResult.StubType);
- Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory);
- Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
- Assert.Equal(result?.ToString(), expectedResult.ToString());
+ Assert.Equal(result!.Path, expectedResult.Path);
+ Assert.Equal(result.Container, expectedResult.Container);
+ Assert.Equal(result.Name, expectedResult.Name);
+ Assert.Equal(result.Year, expectedResult.Year);
+ Assert.Equal(result.ExtraType, expectedResult.ExtraType);
+ Assert.Equal(result.Format3D, expectedResult.Format3D);
+ Assert.Equal(result.Is3D, expectedResult.Is3D);
+ Assert.Equal(result.IsStub, expectedResult.IsStub);
+ Assert.Equal(result.StubType, expectedResult.StubType);
+ Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
+ Assert.Equal(result.FileNameWithoutExtension.ToString(), expectedResult.FileNameWithoutExtension.ToString());
+ Assert.Equal(result.ToString(), expectedResult.ToString());
}
[Fact]
public void ResolveFile_EmptyPath()
{
- var result = _videoResolver.ResolveFile(string.Empty);
+ var result = VideoResolver.ResolveFile(string.Empty, _namingOptions);
Assert.Null(result);
}
@@ -194,7 +194,7 @@ namespace Jellyfin.Naming.Tests.Video
string.Empty
};
- var results = paths.Select(path => _videoResolver.ResolveDirectory(path)).ToList();
+ var results = paths.Select(path => VideoResolver.ResolveDirectory(path, _namingOptions)).ToList();
Assert.Equal(3, results.Count);
Assert.NotNull(results[0]);
diff --git a/tests/Jellyfin.Networking.Tests/IPHostTests.cs b/tests/Jellyfin.Networking.Tests/IPHostTests.cs
new file mode 100644
index 000000000..ec3a1300c
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/IPHostTests.cs
@@ -0,0 +1,53 @@
+using FsCheck;
+using FsCheck.Xunit;
+using MediaBrowser.Common.Net;
+using Xunit;
+
+namespace Jellyfin.Networking.Tests
+{
+ public static class IPHostTests
+ {
+ /// <summary>
+ /// Checks IP address formats.
+ /// </summary>
+ /// <param name="address">IP Address.</param>
+ [Theory]
+ [InlineData("127.0.0.1")]
+ [InlineData("127.0.0.1:123")]
+ [InlineData("localhost")]
+ [InlineData("localhost:1345")]
+ [InlineData("www.google.co.uk")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
+ [InlineData("192.168.1.2/255.255.255.0")]
+ [InlineData("192.168.1.2/24")]
+ public static void TryParse_ValidHostStrings_True(string address)
+ => Assert.True(IPHost.TryParse(address, out _));
+
+ [Property]
+ public static Property TryParse_IPv4Address_True(IPv4Address address)
+ => IPHost.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ [Property]
+ public static Property TryParse_IPv6Address_True(IPv6Address address)
+ => IPHost.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ /// <summary>
+ /// All should be invalid address strings.
+ /// </summary>
+ /// <param name="address">Invalid address strings.</param>
+ [Theory]
+ [InlineData("256.128.0.0.0.1")]
+ [InlineData("127.0.0.1#")]
+ [InlineData("localhost!")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
+ public static void TryParse_InvalidAddressString_False(string address)
+ => Assert.False(IPHost.TryParse(address, out _));
+ }
+}
diff --git a/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs
new file mode 100644
index 000000000..aa2dbc57a
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs
@@ -0,0 +1,49 @@
+using FsCheck;
+using FsCheck.Xunit;
+using MediaBrowser.Common.Net;
+using Xunit;
+
+namespace Jellyfin.Networking.Tests
+{
+ public static class IPNetAddressTests
+ {
+ /// <summary>
+ /// Checks IP address formats.
+ /// </summary>
+ /// <param name="address">IP Address.</param>
+ [Theory]
+ [InlineData("127.0.0.1")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
+ [InlineData("192.168.1.2/255.255.255.0")]
+ [InlineData("192.168.1.2/24")]
+ public static void TryParse_ValidIPStrings_True(string address)
+ => Assert.True(IPNetAddress.TryParse(address, out _));
+
+ [Property]
+ public static Property TryParse_IPv4Address_True(IPv4Address address)
+ => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ [Property]
+ public static Property TryParse_IPv6Address_True(IPv6Address address)
+ => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ /// <summary>
+ /// All should be invalid address strings.
+ /// </summary>
+ /// <param name="address">Invalid address strings.</param>
+ [Theory]
+ [InlineData("256.128.0.0.0.1")]
+ [InlineData("127.0.0.1#")]
+ [InlineData("localhost!")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
+ public static void TryParse_InvalidAddressString_False(string address)
+ => Assert.False(IPNetAddress.TryParse(address, out _));
+ }
+}
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index d5268facc..97bf673ae 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -15,10 +15,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 671b8598d..97c14d463 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -57,66 +57,6 @@ namespace Jellyfin.Networking.Tests
}
/// <summary>
- /// Checks IP address formats.
- /// </summary>
- /// <param name="address">IP Address.</param>
- [Theory]
- [InlineData("127.0.0.1")]
- [InlineData("127.0.0.1:123")]
- [InlineData("localhost")]
- [InlineData("localhost:1345")]
- [InlineData("www.google.co.uk")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
- [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
- [InlineData("fe80::7add:12ff:febb:c67b%16")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
- [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
- [InlineData("192.168.1.2/255.255.255.0")]
- [InlineData("192.168.1.2/24")]
- public void ValidHostStrings(string address)
- {
- Assert.True(IPHost.TryParse(address, out _));
- }
-
- /// <summary>
- /// Checks IP address formats.
- /// </summary>
- /// <param name="address">IP Address.</param>
- [Theory]
- [InlineData("127.0.0.1")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
- [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")]
- [InlineData("fe80::7add:12ff:febb:c67b%16")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
- [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
- [InlineData("192.168.1.2/255.255.255.0")]
- [InlineData("192.168.1.2/24")]
- public void ValidIPStrings(string address)
- {
- Assert.True(IPNetAddress.TryParse(address, out _));
- }
-
- /// <summary>
- /// All should be invalid address strings.
- /// </summary>
- /// <param name="address">Invalid address strings.</param>
- [Theory]
- [InlineData("256.128.0.0.0.1")]
- [InlineData("127.0.0.1#")]
- [InlineData("localhost!")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
- [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
- public void InvalidAddressString(string address)
- {
- Assert.False(IPNetAddress.TryParse(address, out _));
- Assert.False(IPHost.TryParse(address, out _));
- }
-
- /// <summary>
/// Test collection parsing.
/// </summary>
/// <param name="settings">Collection to parse.</param>
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index b37515e78..14bd53db5 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
index 71f8c5181..f312933fb 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
@@ -166,6 +166,38 @@ namespace Jellyfin.Server.Implementations.Tests.Data
};
}
+ public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData()
+ {
+ yield return new object[]
+ {
+ string.Empty,
+ Array.Empty<ItemImageInfo>()
+ };
+
+ yield return new object[]
+ {
+ "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss",
+ new ItemImageInfo[]
+ {
+ new ()
+ {
+ Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
+ Type = ImageType.Primary,
+ DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
+ Width = 1920,
+ Height = 1080,
+ BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
+ }
+ }
+ };
+
+ yield return new object[]
+ {
+ "|",
+ Array.Empty<ItemImageInfo>()
+ };
+ }
+
[Theory]
[MemberData(nameof(DeserializeImages_Valid_TestData))]
public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected)
@@ -184,6 +216,23 @@ namespace Jellyfin.Server.Implementations.Tests.Data
}
[Theory]
+ [MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))]
+ public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected)
+ {
+ var result = _sqliteItemRepository.DeserializeImages(value);
+ Assert.Equal(expected.Length, result.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i].Path, result[i].Path);
+ Assert.Equal(expected[i].Type, result[i].Type);
+ Assert.Equal(expected[i].DateModified, result[i].DateModified);
+ Assert.Equal(expected[i].Width, result[i].Width);
+ Assert.Equal(expected[i].Height, result[i].Height);
+ Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
+ }
+ }
+
+ [Theory]
[MemberData(nameof(DeserializeImages_Valid_TestData))]
public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value)
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index 27713d58a..adbca8344 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -24,7 +24,7 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
@@ -42,6 +42,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
+ <ProjectReference Include="..\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj" />
</ItemGroup>
</Project>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
index 4fa64d8a2..70acbfc40 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System;
using System.IO;
+using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -46,12 +47,36 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
[Fact]
public async Task GetPackages_Valid_Success()
{
- IList<PackageInfo> packages = await _installationManager.GetPackages(
+ PackageInfo[] packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
false);
- Assert.Equal(25, packages.Count);
+ Assert.Equal(25, packages.Length);
+ }
+
+ [Fact]
+ public async Task FilterPackages_NameOnly_Success()
+ {
+ PackageInfo[] packages = await _installationManager.GetPackages(
+ "Jellyfin Stable",
+ "https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
+ false);
+
+ packages = _installationManager.FilterPackages(packages, "Anime").ToArray();
+ Assert.Single(packages);
+ }
+
+ [Fact]
+ public async Task FilterPackages_GuidOnly_Success()
+ {
+ PackageInfo[] packages = await _installationManager.GetPackages(
+ "Jellyfin Stable",
+ "https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
+ false);
+
+ packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
+ Assert.Single(packages);
}
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs
new file mode 100644
index 000000000..9db8689a7
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs
@@ -0,0 +1,14 @@
+using Jellyfin.Api;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ /// <summary>
+ /// Base controller for testing infrastructure.
+ /// Automatically ignored in generated openapi spec.
+ /// </summary>
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public class BaseJellyfinTestController : BaseJellyfinApiController
+ {
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs
new file mode 100644
index 000000000..1a720c2f6
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ /// <summary>
+ /// Controller for testing the encoded url.
+ /// </summary>
+ public class EncoderController : BaseJellyfinTestController
+ {
+ /// <summary>
+ /// Tests the url decoding.
+ /// </summary>
+ /// <param name="params">Parameters to echo back in the response.</param>
+ /// <returns>An <see cref="OkResult"/>.</returns>
+ /// <response code="200">Information retrieved.</response>
+ [HttpGet("UrlDecode")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ContentResult TestUrlDecoding([FromQuery] Dictionary<string, string>? @params = null)
+ {
+ return new ContentResult()
+ {
+ Content = (@params != null && @params.Count > 0)
+ ? string.Join("&", @params.Select(x => x.Key + "=" + x.Value))
+ : string.Empty,
+ ContentType = "text/plain; charset=utf-8",
+ StatusCode = 200
+ };
+ }
+
+ /// <summary>
+ /// Tests the url decoding.
+ /// </summary>
+ /// <param name="params">Parameters to echo back in the response.</param>
+ /// <returns>An <see cref="OkResult"/>.</returns>
+ /// <response code="200">Information retrieved.</response>
+ [HttpGet("UrlArrayDecode")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ContentResult TestUrlArrayDecoding([FromQuery] Dictionary<string, string[]>? @params = null)
+ {
+ return new ContentResult()
+ {
+ Content = (@params != null && @params.Count > 0)
+ ? string.Join("&", @params.Select(x => x.Key + "=" + string.Join(',', x.Value)))
+ : string.Empty,
+ ContentType = "text/plain; charset=utf-8",
+ StatusCode = 200
+ };
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
new file mode 100644
index 000000000..732b4f050
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
@@ -0,0 +1,48 @@
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests
+{
+ /// <summary>
+ /// Defines the test for encoded querystrings in the url.
+ /// </summary>
+ public class EncodedQueryStringTest : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+
+ public EncodedQueryStringTest(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Theory]
+ [InlineData("a=1&b=2&c=3", "a=1&b=2&c=3")] // won't be processed as there is more than 1.
+ [InlineData("a=1", "a=1")] // won't be processed as it has a value
+ [InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed.
+ [InlineData("a=b&a=c", "a=b")]
+ [InlineData("a%3Db%26a%3Dc", "a=b")]
+ public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ Assert.Equal(unencodedUrl, reply);
+ }
+
+ [Theory]
+ [InlineData("a=b&a=c", "a=b,c")]
+ [InlineData("a%3Db%26a%3Dc", "a=b,c")]
+ public async Task Ensure_Array_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ Assert.Equal(unencodedUrl, reply);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 4bf6faef7..59f125cd0 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -12,9 +12,9 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index 260b99df9..c8e72c10d 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -13,9 +13,9 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
new file mode 100644
index 000000000..419afb2dc
--- /dev/null
+++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Server.Middleware;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Jellyfin.Server.Tests
+{
+ public static class UrlDecodeQueryFeatureTests
+ {
+ [Theory]
+ [InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded
+ [InlineData("random+test", "random test")] // encoded
+ [InlineData("random%20test", "random test")] // encoded
+ [InlineData("++", " ")] // encoded
+ public static void EmptyValueTest(string query, string key)
+ {
+ var dict = new Dictionary<string, StringValues>
+ {
+ { query, StringValues.Empty }
+ };
+ var test = new UrlDecodeQueryFeature(new QueryFeature(new QueryCollection(dict)));
+ Assert.Single(test.Query);
+ var (k, v) = test.Query.First();
+ Assert.Equal(key, k);
+ Assert.Empty(v);
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index 4132205c3..0a04a5c54 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index 9ad093a2b..3e726f23d 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -14,8 +14,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
-#pragma warning disable CA5369
-
namespace Jellyfin.XbmcMetadata.Tests.Parsers
{
public class EpisodeNfoProviderTests
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
index 2129f3422..eea8cb50a 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
@@ -1,6 +1,4 @@
-#pragma warning disable CA5369
-
-using System;
+using System;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Audio;
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
index 0e61fa2a1..31110dbd7 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
@@ -1,6 +1,4 @@
-#pragma warning disable CA5369
-
-using System;
+using System;
using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;