aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2021-08-18 02:46:59 -0400
committerGitHub <noreply@github.com>2021-08-18 02:46:59 -0400
commit72d3f7020ad80ce1a53eeae8c5d57abeb22a4679 (patch)
treedd43e663838cdc7d99a4af565523df58ae23c856 /MediaBrowser.Controller
parent7aef0fce444e6d8e06386553ec7ea1401a01bbb1 (diff)
parente5cbafdb6b47377052e0d638908ef96e30a997d6 (diff)
Merge branch 'master' into patch-2
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Authentication/AuthenticationResult.cs2
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs2
-rw-r--r--MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs2
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs137
-rw-r--r--MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs35
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs24
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemInfo.cs30
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemResult.cs11
-rw-r--r--MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs11
-rw-r--r--MediaBrowser.Controller/Channels/ChannelSearchInfo.cs7
-rw-r--r--MediaBrowser.Controller/Channels/IChannel.cs2
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs32
-rw-r--r--MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs12
-rw-r--r--MediaBrowser.Controller/Channels/IHasCacheKey.cs2
-rw-r--r--MediaBrowser.Controller/Channels/IHasFolderAttributes.cs9
-rw-r--r--MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs8
-rw-r--r--MediaBrowser.Controller/Channels/ISearchableChannel.cs34
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsDelete.cs15
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs21
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs9
-rw-r--r--MediaBrowser.Controller/Channels/InternalChannelFeatures.cs10
-rw-r--r--MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs2
-rw-r--r--MediaBrowser.Controller/Chapters/IChapterManager.cs2
-rw-r--r--MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs24
-rw-r--r--MediaBrowser.Controller/Collections/CollectionCreationOptions.cs6
-rw-r--r--MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs (renamed from MediaBrowser.Controller/Collections/CollectionEvents.cs)23
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs7
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs6
-rw-r--r--MediaBrowser.Controller/Dlna/IDlnaManager.cs6
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs12
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs7
-rw-r--r--MediaBrowser.Controller/Drawing/ImageCollageOptions.cs6
-rw-r--r--MediaBrowser.Controller/Drawing/ImageHelper.cs68
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs14
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs2
-rw-r--r--MediaBrowser.Controller/Drawing/ImageStream.cs14
-rw-r--r--MediaBrowser.Controller/Dto/DtoOptions.cs48
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs9
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs59
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs46
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs14
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs56
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs78
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs46
-rw-r--r--MediaBrowser.Controller/Entities/AudioBook.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs1198
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs27
-rw-r--r--MediaBrowser.Controller/Entities/BasePluginFolder.cs14
-rw-r--r--MediaBrowser.Controller/Entities/Book.cs12
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs111
-rw-r--r--MediaBrowser.Controller/Entities/Extensions.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs521
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs54
-rw-r--r--MediaBrowser.Controller/Entities/ICollectionFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/IHasAspectRatio.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasDisplayOrder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasMediaSources.cs6
-rw-r--r--MediaBrowser.Controller/Entities/IHasProgramAttributes.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasScreenshots.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasSeries.cs4
-rw-r--r--MediaBrowser.Controller/Entities/IHasShares.cs11
-rw-r--r--MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasTrailers.cs5
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs214
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs27
-rw-r--r--MediaBrowser.Controller/Entities/ItemImageInfo.cs2
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChild.cs47
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChildComparer.cs35
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChildType.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs57
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs6
-rw-r--r--MediaBrowser.Controller/Entities/MusicVideo.cs14
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs47
-rw-r--r--MediaBrowser.Controller/Entities/PersonInfo.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Photo.cs54
-rw-r--r--MediaBrowser.Controller/Entities/Share.cs7
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs44
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs148
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs111
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs61
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs14
-rw-r--r--MediaBrowser.Controller/Entities/UserItemData.cs18
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs53
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs106
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs98
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs288
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs46
-rw-r--r--MediaBrowser.Controller/Events/IEventConsumer.cs2
-rw-r--r--MediaBrowser.Controller/Events/IEventManager.cs2
-rw-r--r--MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs8
-rw-r--r--MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs6
-rw-r--r--MediaBrowser.Controller/Extensions/StringExtensions.cs57
-rw-r--r--MediaBrowser.Controller/IDisplayPreferencesManager.cs34
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs3
-rw-r--r--MediaBrowser.Controller/IResourceFileManager.cs9
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs66
-rw-r--r--MediaBrowser.Controller/IServerApplicationPaths.cs2
-rw-r--r--MediaBrowser.Controller/Library/DeleteOptions.cs8
-rw-r--r--MediaBrowser.Controller/Library/IIntroProvider.cs14
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs132
-rw-r--r--MediaBrowser.Controller/Library/ILiveStream.cs4
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs24
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceProvider.cs6
-rw-r--r--MediaBrowser.Controller/Library/IMetadataSaver.cs3
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs16
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs22
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs40
-rw-r--r--MediaBrowser.Controller/Library/IUserViewManager.cs23
-rw-r--r--MediaBrowser.Controller/Library/IntroInfo.cs2
-rw-r--r--MediaBrowser.Controller/Library/ItemChangeEventArgs.cs4
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs88
-rw-r--r--MediaBrowser.Controller/Library/LibraryManagerExtensions.cs2
-rw-r--r--MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs17
-rw-r--r--MediaBrowser.Controller/Library/MetadataConfigurationStore.cs12
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs11
-rw-r--r--MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs4
-rw-r--r--MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/Profiler.cs83
-rw-r--r--MediaBrowser.Controller/Library/SearchHintInfo.cs2
-rw-r--r--MediaBrowser.Controller/Library/TVUtils.cs4
-rw-r--r--MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs19
-rw-r--r--MediaBrowser.Controller/LiveTv/ChannelInfo.cs15
-rw-r--r--MediaBrowser.Controller/LiveTv/IListingsProvider.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs52
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvService.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHost.cs6
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs126
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs149
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs3
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ProgramInfo.cs42
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingInfo.cs29
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs31
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerEventInfo.cs1
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerInfo.cs18
-rw-r--r--MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs2
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj36
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs204
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs1772
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs485
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs282
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs9
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs74
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs10
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs12
-rw-r--r--MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs39
-rw-r--r--MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs13
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs23
-rw-r--r--MediaBrowser.Controller/Net/AuthenticatedAttribute.cs76
-rw-r--r--MediaBrowser.Controller/Net/AuthorizationInfo.cs23
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs61
-rw-r--r--MediaBrowser.Controller/Net/IAuthService.cs19
-rw-r--r--MediaBrowser.Controller/Net/IAuthorizationContext.cs10
-rw-r--r--MediaBrowser.Controller/Net/IHasResultFactory.cs17
-rw-r--r--MediaBrowser.Controller/Net/IHttpResultFactory.cs82
-rw-r--r--MediaBrowser.Controller/Net/IHttpServer.cs50
-rw-r--r--MediaBrowser.Controller/Net/ISessionContext.cs8
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs8
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketListener.cs9
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketManager.cs18
-rw-r--r--MediaBrowser.Controller/Net/SecurityException.cs2
-rw-r--r--MediaBrowser.Controller/Net/StaticResultOptions.cs44
-rw-r--r--MediaBrowser.Controller/Net/WebSocketListenerState.cs17
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessageInfo.cs2
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationManager.cs2
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationService.cs2
-rw-r--r--MediaBrowser.Controller/Notifications/UserNotification.cs2
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs18
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs14
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs2
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs112
-rw-r--r--MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs22
-rw-r--r--MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs51
-rw-r--r--MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs9
-rw-r--r--MediaBrowser.Controller/Plugins/IServerEntryPoint.cs8
-rw-r--r--MediaBrowser.Controller/Providers/AlbumInfo.cs16
-rw-r--r--MediaBrowser.Controller/Providers/ArtistInfo.cs6
-rw-r--r--MediaBrowser.Controller/Providers/BookInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs52
-rw-r--r--MediaBrowser.Controller/Providers/DynamicImageResponse.cs2
-rw-r--r--MediaBrowser.Controller/Providers/EpisodeInfo.cs14
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs6
-rw-r--r--MediaBrowser.Controller/Providers/IExternalId.cs2
-rw-r--r--MediaBrowser.Controller/Providers/ILocalImageProvider.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IMetadataService.cs12
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs57
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteImageProvider.cs1
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs1
-rw-r--r--MediaBrowser.Controller/Providers/ImageRefreshOptions.cs22
-rw-r--r--MediaBrowser.Controller/Providers/ItemInfo.cs5
-rw-r--r--MediaBrowser.Controller/Providers/ItemLookupInfo.cs28
-rw-r--r--MediaBrowser.Controller/Providers/LocalImageInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs40
-rw-r--r--MediaBrowser.Controller/Providers/MetadataResult.cs42
-rw-r--r--MediaBrowser.Controller/Providers/MusicVideoInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/RefreshPriority.cs23
-rw-r--r--MediaBrowser.Controller/Providers/RemoteSearchQuery.cs4
-rw-r--r--MediaBrowser.Controller/Providers/SeasonInfo.cs6
-rw-r--r--MediaBrowser.Controller/Providers/SongInfo.cs14
-rw-r--r--MediaBrowser.Controller/QuickConnect/IQuickConnect.cs37
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs22
-rw-r--r--MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs2
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs (renamed from MediaBrowser.Controller/Resolvers/BaseItemResolver.cs)18
-rw-r--r--MediaBrowser.Controller/Resolvers/ResolverPriority.cs7
-rw-r--r--MediaBrowser.Controller/Security/AuthenticationInfo.cs2
-rw-r--r--MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs2
-rw-r--r--MediaBrowser.Controller/Security/IAuthenticationRepository.cs4
-rw-r--r--MediaBrowser.Controller/Session/AuthenticationRequest.cs3
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs11
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs77
-rw-r--r--MediaBrowser.Controller/Session/SessionEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs14
-rw-r--r--MediaBrowser.Controller/Sorting/AlphanumComparator.cs137
-rw-r--r--MediaBrowser.Controller/Sorting/IBaseItemComparer.cs2
-rw-r--r--MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs2
-rw-r--r--MediaBrowser.Controller/Sorting/SortExtensions.cs3
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs29
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleResponse.cs2
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs20
-rw-r--r--MediaBrowser.Controller/Sync/IHasDynamicAccess.cs20
-rw-r--r--MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs9
-rw-r--r--MediaBrowser.Controller/Sync/IServerSyncProvider.cs30
-rw-r--r--MediaBrowser.Controller/Sync/ISyncProvider.cs29
-rw-r--r--MediaBrowser.Controller/Sync/SyncedFileInfo.cs41
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupInfo.cs170
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupMember.cs29
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs224
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs128
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs167
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs170
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs682
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs29
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupState.cs219
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs224
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs67
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs37
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayRequest.cs16
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs31
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs63
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs38
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs47
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs39
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs23
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs38
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs56
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs39
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs48
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs63
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs40
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs38
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs39
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs38
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs38
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/StopGroupRequest.cs21
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/UnpauseGroupRequest.cs21
-rw-r--r--MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs579
-rw-r--r--MediaBrowser.Controller/SyncPlay/Requests/JoinGroupRequest.cs29
-rw-r--r--MediaBrowser.Controller/SyncPlay/Requests/LeaveGroupRequest.cs13
-rw-r--r--MediaBrowser.Controller/SyncPlay/Requests/ListGroupsRequest.cs13
-rw-r--r--MediaBrowser.Controller/SyncPlay/Requests/NewGroupRequest.cs28
-rw-r--r--MediaBrowser.Controller/TV/ITVSeriesManager.cs10
275 files changed, 8885 insertions, 4937 deletions
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
index 4249a9a667..635e4eb3d7 100644
--- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
+++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Controller.Session;
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
index ecdffa2ebe..a56d3c8223 100644
--- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Threading.Tasks;
diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
index 6729b91157..8c9d1baf88 100644
--- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
new file mode 100644
index 0000000000..abfdb41d80
--- /dev/null
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.BaseItemManager
+{
+ /// <inheritdoc />
+ public class BaseItemManager : IBaseItemManager
+ {
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ private int _metadataRefreshConcurrency;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseItemManager"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public BaseItemManager(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+
+ _metadataRefreshConcurrency = GetMetadataRefreshConcurrency();
+ SetupMetadataThrottler();
+
+ _serverConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
+ }
+
+ /// <inheritdoc />
+ public SemaphoreSlim MetadataRefreshThrottler { get; private set; }
+
+ /// <inheritdoc />
+ public bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name)
+ {
+ if (baseItem is Channel)
+ {
+ // Hack alert.
+ return true;
+ }
+
+ if (baseItem.SourceType == SourceType.Channel)
+ {
+ // Hack alert.
+ return !baseItem.EnableMediaSourceDisplay;
+ }
+
+ var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
+ if (typeOptions != null)
+ {
+ return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (!libraryOptions.EnableInternetProviders)
+ {
+ return false;
+ }
+
+ var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+
+ return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <inheritdoc />
+ public bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name)
+ {
+ if (baseItem is Channel)
+ {
+ // Hack alert.
+ return true;
+ }
+
+ if (baseItem.SourceType == SourceType.Channel)
+ {
+ // Hack alert.
+ return !baseItem.EnableMediaSourceDisplay;
+ }
+
+ var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
+ if (typeOptions != null)
+ {
+ return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (!libraryOptions.EnableInternetProviders)
+ {
+ return false;
+ }
+
+ var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+
+ return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Called when the configuration is updated.
+ /// It will refresh the metadata throttler if the relevant config changed.
+ /// </summary>
+ private void OnConfigurationUpdated(object? sender, EventArgs e)
+ {
+ int newMetadataRefreshConcurrency = GetMetadataRefreshConcurrency();
+ if (_metadataRefreshConcurrency != newMetadataRefreshConcurrency)
+ {
+ _metadataRefreshConcurrency = newMetadataRefreshConcurrency;
+ SetupMetadataThrottler();
+ }
+ }
+
+ /// <summary>
+ /// Creates the metadata refresh throttler.
+ /// </summary>
+ [MemberNotNull(nameof(MetadataRefreshThrottler))]
+ private void SetupMetadataThrottler()
+ {
+ MetadataRefreshThrottler = new SemaphoreSlim(_metadataRefreshConcurrency);
+ }
+
+ /// <summary>
+ /// Returns the metadata refresh concurrency.
+ /// </summary>
+ private int GetMetadataRefreshConcurrency()
+ {
+ var concurrency = _serverConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency;
+
+ if (concurrency <= 0)
+ {
+ concurrency = Environment.ProcessorCount;
+ }
+
+ return concurrency;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
new file mode 100644
index 0000000000..e18994214e
--- /dev/null
+++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
@@ -0,0 +1,35 @@
+using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.BaseItemManager
+{
+ /// <summary>
+ /// The <c>BaseItem</c> manager.
+ /// </summary>
+ public interface IBaseItemManager
+ {
+ /// <summary>
+ /// Gets the semaphore used to limit the amount of concurrent metadata refreshes.
+ /// </summary>
+ SemaphoreSlim MetadataRefreshThrottler { get; }
+
+ /// <summary>
+ /// Is metadata fetcher enabled.
+ /// </summary>
+ /// <param name="baseItem">The base item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="name">The metadata fetcher name.</param>
+ /// <returns><c>true</c> if metadata fetcher is enabled, else false.</returns>
+ bool IsMetadataFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name);
+
+ /// <summary>
+ /// Is image fetcher enabled.
+ /// </summary>
+ /// <param name="baseItem">The base item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="name">The image fetcher name.</param>
+ /// <returns><c>true</c> if image fetcher is enabled, else false.</returns>
+ bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name);
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index 129cdb519f..e6923b55ca 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -15,11 +17,18 @@ namespace MediaBrowser.Controller.Channels
{
public class Channel : Folder
{
+ [JsonIgnore]
+ public override bool SupportsInheritedParentImages => false;
+
+ [JsonIgnore]
+ public override SourceType SourceType => SourceType.Channel;
+
public override bool IsVisible(User user)
{
- if (user.GetPreference(PreferenceKind.BlockedChannels) != null)
+ var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
+ if (blockedChannelsPreference.Length != 0)
{
- if (user.GetPreference(PreferenceKind.BlockedChannels).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ if (blockedChannelsPreference.Contains(Id))
{
return false;
}
@@ -27,8 +36,7 @@ namespace MediaBrowser.Controller.Channels
else
{
if (!user.HasPermission(PermissionKind.EnableAllChannels)
- && !user.GetPreference(PreferenceKind.EnabledChannels)
- .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels).Contains(Id))
{
return false;
}
@@ -37,12 +45,6 @@ namespace MediaBrowser.Controller.Channels
return base.IsVisible(user);
}
- [JsonIgnore]
- public override bool SupportsInheritedParentImages => false;
-
- [JsonIgnore]
- public override SourceType SourceType => SourceType.Channel;
-
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
try
@@ -82,7 +84,7 @@ namespace MediaBrowser.Controller.Channels
internal static bool IsChannelVisible(BaseItem channelItem, User user)
{
- var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(""));
+ var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(string.Empty));
return channel.IsVisible(user);
}
diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
index 476992cbd4..55f80b240f 100644
--- a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
+++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
@@ -11,6 +13,19 @@ namespace MediaBrowser.Controller.Channels
{
public class ChannelItemInfo : IHasProviderIds
{
+ public ChannelItemInfo()
+ {
+ MediaSources = new List<MediaSourceInfo>();
+ TrailerTypes = new List<TrailerType>();
+ Genres = new List<string>();
+ Studios = new List<string>();
+ People = new List<PersonInfo>();
+ Tags = new List<string>();
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ Artists = new List<string>();
+ AlbumArtists = new List<string>();
+ }
+
public string Name { get; set; }
public string SeriesName { get; set; }
@@ -78,18 +93,5 @@ namespace MediaBrowser.Controller.Channels
public bool IsLiveStream { get; set; }
public string Etag { get; set; }
-
- public ChannelItemInfo()
- {
- MediaSources = new List<MediaSourceInfo>();
- TrailerTypes = new List<TrailerType>();
- Genres = new List<string>();
- Studios = new List<string>();
- People = new List<PersonInfo>();
- Tags = new List<string>();
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- Artists = new List<string>();
- AlbumArtists = new List<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs
index cee7b20039..ca7721991d 100644
--- a/MediaBrowser.Controller/Channels/ChannelItemResult.cs
+++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs
@@ -1,18 +1,19 @@
#pragma warning disable CS1591
+using System;
using System.Collections.Generic;
namespace MediaBrowser.Controller.Channels
{
public class ChannelItemResult
{
- public List<ChannelItemInfo> Items { get; set; }
-
- public int? TotalRecordCount { get; set; }
-
public ChannelItemResult()
{
- Items = new List<ChannelItemInfo>();
+ Items = Array.Empty<ChannelItemInfo>();
}
+
+ public IReadOnlyList<ChannelItemInfo> Items { get; set; }
+
+ public int? TotalRecordCount { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs b/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs
new file mode 100644
index 0000000000..6f0761e64b
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs
@@ -0,0 +1,11 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Controller.Channels
+{
+ public class ChannelLatestMediaSearch
+ {
+ public string UserId { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
index 32469d4d7b..990b025bcb 100644
--- a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
+++ b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
@@ -8,9 +10,4 @@ namespace MediaBrowser.Controller.Channels
public string UserId { get; set; }
}
-
- public class ChannelLatestMediaSearch
- {
- public string UserId { get; set; }
- }
}
diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs
index 2c0eadf950..01bf8d5c85 100644
--- a/MediaBrowser.Controller/Channels/IChannel.cs
+++ b/MediaBrowser.Controller/Channels/IChannel.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs
index 9a9d22d33d..49be897ef3 100644
--- a/MediaBrowser.Controller/Channels/IChannelManager.cs
+++ b/MediaBrowser.Controller/Channels/IChannelManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -24,7 +26,7 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>ChannelFeatures.</returns>
- ChannelFeatures GetChannelFeatures(string id);
+ ChannelFeatures GetChannelFeatures(Guid? id);
/// <summary>
/// Gets all channel features.
@@ -49,32 +51,47 @@ namespace MediaBrowser.Controller.Channels
/// Gets the channels internal.
/// </summary>
/// <param name="query">The query.</param>
+ /// <returns>The channels.</returns>
QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
/// <summary>
/// Gets the channels.
/// </summary>
/// <param name="query">The query.</param>
+ /// <returns>The channels.</returns>
QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
/// <summary>
- /// Gets the latest media.
+ /// Gets the latest channel items.
/// </summary>
+ /// <param name="query">The item query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The latest channels.</returns>
Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
/// <summary>
- /// Gets the latest media.
+ /// Gets the latest channel items.
/// </summary>
+ /// <param name="query">The item query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The latest channels.</returns>
Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken);
/// <summary>
/// Gets the channel items.
/// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The channel items.</returns>
Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
/// <summary>
- /// Gets the channel items internal.
+ /// Gets the channel items.
/// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="progress">The progress to report to.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The channel items.</returns>
Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
@@ -82,9 +99,14 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns>
+ /// <returns>The item media sources.</returns>
IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken);
+ /// <summary>
+ /// Whether the item supports media probe.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>Whether media probe should be enabled.</returns>
bool EnableMediaProbe(BaseItem item);
}
}
diff --git a/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
new file mode 100644
index 0000000000..0539b9048b
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
@@ -0,0 +1,12 @@
+namespace MediaBrowser.Controller.Channels
+{
+ /// <summary>
+ /// Disable media source display.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="Channel"/> can inherit this interface to disable being displayed.
+ /// </remarks>
+ public interface IDisableMediaSourceDisplay
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
index bf895a0eca..9fae43033e 100644
--- a/MediaBrowser.Controller/Channels/IHasCacheKey.cs
+++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
diff --git a/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs
new file mode 100644
index 0000000000..64af8496c7
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs
@@ -0,0 +1,9 @@
+#pragma warning disable CA1819, CS1591
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface IHasFolderAttributes
+ {
+ string[] Attributes { get; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
index 589295543c..eeaa6b622e 100644
--- a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
+++ b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -7,11 +5,17 @@ using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.Channels
{
+ /// <summary>
+ /// The channel requires a media info callback.
+ /// </summary>
public interface IRequiresMediaInfoCallback
{
/// <summary>
/// Gets the channel item media information.
/// </summary>
+ /// <param name="id">The channel item id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The enumerable of media source info.</returns>
Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaInfo(string id, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs
index b627ca1c25..b87943a6ed 100644
--- a/MediaBrowser.Controller/Channels/ISearchableChannel.cs
+++ b/MediaBrowser.Controller/Channels/ISearchableChannel.cs
@@ -1,9 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Channels
{
@@ -17,35 +18,4 @@ namespace MediaBrowser.Controller.Channels
/// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken);
}
-
- public interface ISupportsLatestMedia
- {
- /// <summary>
- /// Gets the latest media.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
- Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken);
- }
-
- public interface ISupportsDelete
- {
- bool CanDelete(BaseItem item);
-
- Task DeleteItem(string id, CancellationToken cancellationToken);
- }
-
- public interface IDisableMediaSourceDisplay
- {
- }
-
- public interface ISupportsMediaProbe
- {
- }
-
- public interface IHasFolderAttributes
- {
- string[] Attributes { get; }
- }
}
diff --git a/MediaBrowser.Controller/Channels/ISupportsDelete.cs b/MediaBrowser.Controller/Channels/ISupportsDelete.cs
new file mode 100644
index 0000000000..204054374e
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ISupportsDelete.cs
@@ -0,0 +1,15 @@
+#pragma warning disable CS1591
+
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface ISupportsDelete
+ {
+ bool CanDelete(BaseItem item);
+
+ Task DeleteItem(string id, CancellationToken cancellationToken);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
new file mode 100644
index 0000000000..dbba7cba2d
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
@@ -0,0 +1,21 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface ISupportsLatestMedia
+ {
+ /// <summary>
+ /// Gets the latest media.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The latest media.</returns>
+ Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
new file mode 100644
index 0000000000..bc7683125b
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Channels
+{
+ /// <summary>
+ /// Channel supports media probe.
+ /// </summary>
+ public interface ISupportsMediaProbe
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
index 1074ce435e..394996868e 100644
--- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
+++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CA2227, CS1591
using System.Collections.Generic;
using MediaBrowser.Model.Channels;
@@ -28,7 +30,7 @@ namespace MediaBrowser.Controller.Channels
public List<ChannelMediaContentType> ContentTypes { get; set; }
/// <summary>
- /// Represents the maximum number of records the channel allows retrieving at a time.
+ /// Gets or sets the maximum number of records the channel allows retrieving at a time.
/// </summary>
public int? MaxPageSize { get; set; }
@@ -39,9 +41,10 @@ namespace MediaBrowser.Controller.Channels
public List<ChannelItemSortField> DefaultSortFields { get; set; }
/// <summary>
- /// Indicates if a sort ascending/descending toggle is supported or not.
+ /// Gets or sets a value indicating whether a sort ascending/descending toggle is supported or not.
/// </summary>
public bool SupportsSortOrderToggle { get; set; }
+
/// <summary>
/// Gets or sets the automatic refresh levels.
/// </summary>
@@ -53,6 +56,7 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <value>The daily download limit.</value>
public int? DailyDownloadLimit { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether [supports downloading].
/// </summary>
diff --git a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
index 7e9bb28ed6..0d837faca2 100644
--- a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
+++ b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs
index f82e5b41a2..c049bb97e7 100644
--- a/MediaBrowser.Controller/Chapters/IChapterManager.cs
+++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs
@@ -12,6 +12,8 @@ namespace MediaBrowser.Controller.Chapters
/// <summary>
/// Saves the chapters.
/// </summary>
+ /// <param name="itemId">The item.</param>
+ /// <param name="chapters">The set of chapters.</param>
void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
}
}
diff --git a/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs b/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs
new file mode 100644
index 0000000000..82b3a49773
--- /dev/null
+++ b/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs
@@ -0,0 +1,24 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using MediaBrowser.Controller.Entities.Movies;
+
+namespace MediaBrowser.Controller.Collections
+{
+ public class CollectionCreatedEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the collection.
+ /// </summary>
+ /// <value>The collection.</value>
+ public BoxSet Collection { get; set; }
+
+ /// <summary>
+ /// Gets or sets the options.
+ /// </summary>
+ /// <value>The options.</value>
+ public CollectionCreationOptions Options { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
index f6037d05e1..30f5f4efa2 100644
--- a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
+++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -23,8 +25,8 @@ namespace MediaBrowser.Controller.Collections
public Dictionary<string, string> ProviderIds { get; set; }
- public string[] ItemIdList { get; set; }
+ public IReadOnlyList<string> ItemIdList { get; set; }
- public Guid[] UserIds { get; set; }
+ public IReadOnlyList<Guid> UserIds { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Collections/CollectionEvents.cs b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs
index ce59b4ada2..e538fa4b3f 100644
--- a/MediaBrowser.Controller/Collections/CollectionEvents.cs
+++ b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs
@@ -7,23 +7,14 @@ using MediaBrowser.Controller.Entities.Movies;
namespace MediaBrowser.Controller.Collections
{
- public class CollectionCreatedEventArgs : EventArgs
- {
- /// <summary>
- /// Gets or sets the collection.
- /// </summary>
- /// <value>The collection.</value>
- public BoxSet Collection { get; set; }
-
- /// <summary>
- /// Gets or sets the options.
- /// </summary>
- /// <value>The options.</value>
- public CollectionCreationOptions Options { get; set; }
- }
-
public class CollectionModifiedEventArgs : EventArgs
{
+ public CollectionModifiedEventArgs(BoxSet collection, IReadOnlyCollection<BaseItem> itemsChanged)
+ {
+ Collection = collection;
+ ItemsChanged = itemsChanged;
+ }
+
/// <summary>
/// Gets or sets the collection.
/// </summary>
@@ -34,6 +25,6 @@ namespace MediaBrowser.Controller.Collections
/// Gets or sets the items changed.
/// </summary>
/// <value>The items changed.</value>
- public List<BaseItem> ItemsChanged { get; set; }
+ public IReadOnlyCollection<BaseItem> ItemsChanged { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index a6991e2eac..b8c33ee5a0 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -14,22 +14,23 @@ namespace MediaBrowser.Controller.Collections
/// <summary>
/// Occurs when [collection created].
/// </summary>
- event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+ event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
/// <summary>
/// Occurs when [items added to collection].
/// </summary>
- event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+ event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
/// <summary>
/// Occurs when [items removed from collection].
/// </summary>
- event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+ event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
/// <summary>
/// Creates the collection.
/// </summary>
/// <param name="options">The options.</param>
+ /// <returns>BoxSet wrapped in an awaitable task.</returns>
Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options);
/// <summary>
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index 8f0872dba9..8096be1bd3 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -18,7 +20,6 @@ namespace MediaBrowser.Controller.Devices
/// </summary>
/// <param name="reportedId">The reported identifier.</param>
/// <param name="capabilities">The capabilities.</param>
- /// <returns>Task.</returns>
void SaveCapabilities(string reportedId, ClientCapabilities capabilities);
/// <summary>
@@ -45,6 +46,9 @@ namespace MediaBrowser.Controller.Devices
/// <summary>
/// Determines whether this instance [can access device] the specified user identifier.
/// </summary>
+ /// <param name="user">The user to test.</param>
+ /// <param name="deviceId">The device id to test.</param>
+ /// <returns>Whether the user can access the device.</returns>
bool CanAccessDevice(User user, string deviceId);
void UpdateDeviceOptions(string deviceId, DeviceOptions options);
diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
index dc2d5a356a..a64919700d 100644
--- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs
+++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.Dlna
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>DeviceProfile.</returns>
- DeviceProfile GetProfile(IHeaderDictionary headers);
+ DeviceProfile? GetProfile(IHeaderDictionary headers);
/// <summary>
/// Gets the default profile.
@@ -51,14 +51,14 @@ namespace MediaBrowser.Controller.Dlna
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>DeviceProfile.</returns>
- DeviceProfile GetProfile(string id);
+ DeviceProfile? GetProfile(string id);
/// <summary>
/// Gets the profile.
/// </summary>
/// <param name="deviceInfo">The device information.</param>
/// <returns>DeviceProfile.</returns>
- DeviceProfile GetProfile(DeviceIdentification deviceInfo);
+ DeviceProfile? GetProfile(DeviceIdentification deviceInfo);
/// <summary>
/// Gets the server description XML.
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index f9b2e6fef3..4e67cfee4f 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -57,12 +57,22 @@ namespace MediaBrowser.Controller.Drawing
/// <summary>
/// Encode an image.
/// </summary>
+ /// <param name="inputPath">Input path of image.</param>
+ /// <param name="dateModified">Date modified.</param>
+ /// <param name="outputPath">Output path of image.</param>
+ /// <param name="autoOrient">Auto-orient image.</param>
+ /// <param name="orientation">Desired orientation of image.</param>
+ /// <param name="quality">Quality of encoded image.</param>
+ /// <param name="options">Image processing options.</param>
+ /// <param name="outputFormat">Image format of output.</param>
+ /// <returns>Path of encoded image.</returns>
string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
/// <summary>
/// Create an image collage.
/// </summary>
/// <param name="options">The options to use when creating the collage.</param>
- void CreateImageCollage(ImageCollageOptions options);
+ /// <param name="libraryName">Optional. </param>
+ void CreateImageCollage(ImageCollageOptions options, string? libraryName);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index b7edb10524..c7f61a90bb 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -60,7 +60,7 @@ namespace MediaBrowser.Controller.Drawing
string GetImageCacheTag(BaseItem item, ChapterInfo info);
- string GetImageCacheTag(User user);
+ string? GetImageCacheTag(User user);
/// <summary>
/// Processes the image.
@@ -75,7 +75,7 @@ namespace MediaBrowser.Controller.Drawing
/// </summary>
/// <param name="options">The options.</param>
/// <returns>Task.</returns>
- Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
+ Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
/// <summary>
/// Gets the supported image output formats.
@@ -87,7 +87,8 @@ namespace MediaBrowser.Controller.Drawing
/// Creates the image collage.
/// </summary>
/// <param name="options">The options.</param>
- void CreateImageCollage(ImageCollageOptions options);
+ /// <param name="libraryName">The library name to draw onto the collage.</param>
+ void CreateImageCollage(ImageCollageOptions options, string? libraryName);
bool SupportsTransparency(string path);
}
diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
index fe0465d0d7..e9c88ffb56 100644
--- a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
@@ -1,3 +1,7 @@
+#nullable disable
+
+using System.Collections.Generic;
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Drawing
@@ -8,7 +12,7 @@ namespace MediaBrowser.Controller.Drawing
/// Gets or sets the input paths.
/// </summary>
/// <value>The input paths.</value>
- public string[] InputPaths { get; set; }
+ public IReadOnlyList<string> InputPaths { get; set; }
/// <summary>
/// Gets or sets the output path.
diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs
index 87c28d5773..9ef92bc981 100644
--- a/MediaBrowser.Controller/Drawing/ImageHelper.cs
+++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs
@@ -1,75 +1,17 @@
#pragma warning disable CS1591
-using System;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
-using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Drawing
{
public static class ImageHelper
{
- public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions? originalImageSize)
+ public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions originalImageSize)
{
- if (originalImageSize.HasValue)
- {
- // Determine the output size based on incoming parameters
- var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
-
- return newSize;
- }
-
- return GetSizeEstimate(options);
- }
-
- private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options)
- {
- if (options.Width.HasValue && options.Height.HasValue)
- {
- return new ImageDimensions(options.Width.Value, options.Height.Value);
- }
-
- double aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item);
-
- int? width = options.Width ?? options.MaxWidth;
-
- if (width.HasValue)
- {
- int heightValue = Convert.ToInt32((double)width.Value / aspect);
- return new ImageDimensions(width.Value, heightValue);
- }
-
- var height = options.Height ?? options.MaxHeight ?? 200;
- int widthValue = Convert.ToInt32(aspect * height);
- return new ImageDimensions(widthValue, height);
- }
-
- private static double GetEstimatedAspectRatio(ImageType type, BaseItem item)
- {
- switch (type)
- {
- case ImageType.Art:
- case ImageType.Backdrop:
- case ImageType.Chapter:
- case ImageType.Screenshot:
- case ImageType.Thumb:
- return 1.78;
- case ImageType.Banner:
- return 5.4;
- case ImageType.Box:
- case ImageType.BoxRear:
- case ImageType.Disc:
- case ImageType.Menu:
- case ImageType.Profile:
- return 1;
- case ImageType.Logo:
- return 2.58;
- case ImageType.Primary:
- double defaultPrimaryImageAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
- return defaultPrimaryImageAspectRatio > 0 ? defaultPrimaryImageAspectRatio : 2.0 / 3;
- default:
- return 1;
- }
+ // Determine the output size based on incoming parameters
+ var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
+ newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight);
+ return newSize;
}
}
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
index 22105b7d79..11e6633011 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -24,8 +26,6 @@ namespace MediaBrowser.Controller.Drawing
public int ImageIndex { get; set; }
- public bool CropWhiteSpace { get; set; }
-
public int? Width { get; set; }
public int? Height { get; set; }
@@ -34,6 +34,10 @@ namespace MediaBrowser.Controller.Drawing
public int? MaxHeight { get; set; }
+ public int? FillWidth { get; set; }
+
+ public int? FillHeight { get; set; }
+
public int Quality { get; set; }
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
@@ -95,6 +99,11 @@ namespace MediaBrowser.Controller.Drawing
return false;
}
+ if (sizeValue.Width > FillWidth || sizeValue.Height > FillHeight)
+ {
+ return false;
+ }
+
return true;
}
@@ -106,7 +115,6 @@ namespace MediaBrowser.Controller.Drawing
PercentPlayed.Equals(0) &&
!UnplayedCount.HasValue &&
!Blur.HasValue &&
- !CropWhiteSpace &&
string.IsNullOrEmpty(BackgroundColor) &&
string.IsNullOrEmpty(ForegroundLayer);
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
index d3a2b4dbf2..b036425ab3 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs
index 46f58ec159..5d552170f9 100644
--- a/MediaBrowser.Controller/Drawing/ImageStream.cs
+++ b/MediaBrowser.Controller/Drawing/ImageStream.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1711, CS1591
using System;
using System.IO;
@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Drawing
/// Gets or sets the stream.
/// </summary>
/// <value>The stream.</value>
- public Stream Stream { get; set; }
+ public Stream? Stream { get; set; }
/// <summary>
/// Gets or sets the format.
@@ -22,9 +22,15 @@ namespace MediaBrowser.Controller.Drawing
public void Dispose()
{
- if (Stream != null)
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
{
- Stream.Dispose();
+ Stream?.Dispose();
}
}
}
diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
index 76f20ace2a..ecc833154d 100644
--- a/MediaBrowser.Controller/Dto/DtoOptions.cs
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -1,6 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@@ -15,37 +18,17 @@ namespace MediaBrowser.Controller.Dto
ItemFields.RefreshState
};
- public ItemFields[] Fields { get; set; }
-
- public ImageType[] ImageTypes { get; set; }
-
- public int ImageTypeLimit { get; set; }
-
- public bool EnableImages { get; set; }
-
- public bool AddProgramRecordingInfo { get; set; }
-
- public bool EnableUserData { get; set; }
+ private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
- public bool AddCurrentProgram { get; set; }
+ private static readonly ItemFields[] AllItemFields = Enum.GetValues<ItemFields>()
+ .Except(DefaultExcludedFields)
+ .ToArray();
public DtoOptions()
: this(true)
{
}
- private static readonly ImageType[] AllImageTypes = Enum.GetNames(typeof(ImageType))
- .Select(i => (ImageType)Enum.Parse(typeof(ImageType), i, true))
- .ToArray();
-
- private static readonly ItemFields[] AllItemFields = Enum.GetNames(typeof(ItemFields))
- .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
- .Except(DefaultExcludedFields)
- .ToArray();
-
- public bool ContainsField(ItemFields field)
- => Fields.Contains(field);
-
public DtoOptions(bool allFields)
{
ImageTypeLimit = int.MaxValue;
@@ -57,6 +40,23 @@ namespace MediaBrowser.Controller.Dto
ImageTypes = AllImageTypes;
}
+ public IReadOnlyList<ItemFields> Fields { get; set; }
+
+ public IReadOnlyList<ImageType> ImageTypes { get; set; }
+
+ public int ImageTypeLimit { get; set; }
+
+ public bool EnableImages { get; set; }
+
+ public bool AddProgramRecordingInfo { get; set; }
+
+ public bool EnableUserData { get; set; }
+
+ public bool AddCurrentProgram { get; set; }
+
+ public bool ContainsField(ItemFields field)
+ => Fields.Contains(field);
+
public int GetImageLimit(ImageType type)
{
if (EnableImages && ImageTypes.Contains(type))
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index 988557f42c..89aafc84fb 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -1,3 +1,6 @@
+#nullable disable
+#pragma warning disable CA1002
+
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
@@ -34,11 +37,17 @@ namespace MediaBrowser.Controller.Dto
/// <param name="options">The options.</param>
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
+ /// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns>
IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
/// <summary>
/// Gets the item by name dto.
/// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="options">The dto options.</param>
+ /// <param name="taggedItems">The list of tagged items.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>The item dto.</returns>
BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null);
}
}
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index 6ebea5f449..9589f52452 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1819, CS1591
using System;
using System.Collections.Concurrent;
@@ -16,30 +18,23 @@ namespace MediaBrowser.Controller.Entities
{
/// <summary>
/// Specialized folder that can have items added to it's children by external entities.
- /// Used for our RootFolder so plug-ins can add items.
+ /// Used for our RootFolder so plugins can add items.
/// </summary>
public class AggregateFolder : Folder
{
- public AggregateFolder()
- {
- PhysicalLocationsList = Array.Empty<string>();
- }
-
- [JsonIgnore]
- public override bool IsPhysicalRoot => true;
-
- public override bool CanDelete()
- {
- return false;
- }
-
- [JsonIgnore]
- public override bool SupportsPlayedStatus => false;
+ private readonly object _childIdsLock = new object();
/// <summary>
/// The _virtual children.
/// </summary>
private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
+ private bool _requiresRefresh;
+ private Guid[] _childrenIds = null;
+
+ public AggregateFolder()
+ {
+ PhysicalLocationsList = Array.Empty<string>();
+ }
/// <summary>
/// Gets the virtual children.
@@ -48,18 +43,26 @@ namespace MediaBrowser.Controller.Entities
public ConcurrentBag<BaseItem> VirtualChildren => _virtualChildren;
[JsonIgnore]
+ public override bool IsPhysicalRoot => true;
+
+ [JsonIgnore]
+ public override bool SupportsPlayedStatus => false;
+
+ [JsonIgnore]
public override string[] PhysicalLocations => PhysicalLocationsList;
public string[] PhysicalLocationsList { get; set; }
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
{
return CreateResolveArgs(directoryService, true).FileSystemChildren;
}
- private Guid[] _childrenIds = null;
- private readonly object _childIdsLock = new object();
-
protected override List<BaseItem> LoadChildren()
{
lock (_childIdsLock)
@@ -83,7 +86,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- private bool _requiresRefresh;
public override bool RequiresRefresh()
{
var changed = base.RequiresRefresh() || _requiresRefresh;
@@ -103,11 +105,11 @@ namespace MediaBrowser.Controller.Entities
return changed;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
ClearCache();
- var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh;
+ var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh;
_requiresRefresh = false;
return changed;
}
@@ -120,8 +122,7 @@ namespace MediaBrowser.Controller.Entities
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
{
- FileInfo = FileSystem.GetDirectoryInfo(path),
- Path = path
+ FileInfo = FileSystem.GetDirectoryInfo(path)
};
// Gather child folder and files
@@ -153,11 +154,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();
@@ -167,7 +168,7 @@ namespace MediaBrowser.Controller.Entities
/// Adds the virtual child.
/// </summary>
/// <param name="child">The child.</param>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if child is null.</exception>
public void AddVirtualChild(BaseItem child)
{
if (child == null)
@@ -183,7 +184,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 2c6dea02cd..7bf1219ec2 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -1,7 +1,10 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CA1724, CA1826, CS1591
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
@@ -22,6 +25,12 @@ namespace MediaBrowser.Controller.Entities.Audio
IHasLookupInfo<SongInfo>,
IHasMediaSources
{
+ public Audio()
+ {
+ Artists = Array.Empty<string>();
+ AlbumArtists = Array.Empty<string>();
+ }
+
/// <inheritdoc />
[JsonIgnore]
public IReadOnlyList<string> Artists { get; set; }
@@ -30,17 +39,6 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public IReadOnlyList<string> AlbumArtists { get; set; }
- public Audio()
- {
- Artists = Array.Empty<string>();
- AlbumArtists = Array.Empty<string>();
- }
-
- public override double GetDefaultPrimaryImageAspectRatio()
- {
- return 1;
- }
-
[JsonIgnore]
public override bool SupportsPlayedStatus => true;
@@ -59,11 +57,6 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public override Folder LatestItemsIndexContainer => AlbumEntity;
- public override bool CanDownload()
- {
- return IsFileProtocol;
- }
-
[JsonIgnore]
public MusicAlbum AlbumEntity => FindParent<MusicAlbum>();
@@ -74,26 +67,35 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public override string MediaType => Model.Entities.MediaType.Audio;
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ public override bool CanDownload()
+ {
+ return IsFileProtocol;
+ }
+
/// <summary>
/// Creates the name of the sort.
/// </summary>
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "")
- + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty)
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
}
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
- var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty;
-
+ var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : string.Empty;
if (ParentIndexNumber.HasValue)
{
- songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey;
+ songKey = ParentIndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) + "-" + songKey;
}
songKey += Name;
diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
index f6d3cd6cc2..1625c748a8 100644
--- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
@@ -1,6 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Entities.Audio
{
@@ -23,15 +27,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public static IEnumerable<string> GetAllArtists<T>(this T item)
where T : IHasArtist, IHasAlbumArtist
{
- foreach (var i in item.AlbumArtists)
- {
- yield return i;
- }
-
- foreach (var i in item.Artists)
- {
- yield return i;
- }
+ return item.AlbumArtists.Concat(item.Artists).DistinctNames();
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
index ac4dd1688b..c2dae5a2dc 100644
--- a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
+++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1819, CS1591
namespace MediaBrowser.Controller.Entities.Audio
{
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 48cd9371a0..03d1f33043 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1721, CA1826, CS1591
using System;
using System.Collections.Generic;
@@ -21,18 +23,18 @@ namespace MediaBrowser.Controller.Entities.Audio
/// </summary>
public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
{
- /// <inheritdoc />
- public IReadOnlyList<string> AlbumArtists { get; set; }
-
- /// <inheritdoc />
- public IReadOnlyList<string> Artists { get; set; }
-
public MusicAlbum()
{
Artists = Array.Empty<string>();
AlbumArtists = Array.Empty<string>();
}
+ /// <inheritdoc />
+ public IReadOnlyList<string> AlbumArtists { get; set; }
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> Artists { get; set; }
+
[JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
@@ -42,6 +44,25 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public MusicArtist MusicArtist => GetMusicArtist(new DtoOptions(true));
+ [JsonIgnore]
+ public override bool SupportsPlayedStatus => false;
+
+ [JsonIgnore]
+ public override bool SupportsCumulativeRunTimeTicks => true;
+
+ [JsonIgnore]
+ public string AlbumArtist => AlbumArtists.FirstOrDefault();
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ /// <summary>
+ /// Gets the tracks.
+ /// </summary>
+ /// <value>The tracks.</value>
+ [JsonIgnore]
+ public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>();
+
public MusicArtist GetMusicArtist(DtoOptions options)
{
var parents = GetParents();
@@ -62,25 +83,6 @@ namespace MediaBrowser.Controller.Entities.Audio
return null;
}
- [JsonIgnore]
- public override bool SupportsPlayedStatus => false;
-
- [JsonIgnore]
- public override bool SupportsCumulativeRunTimeTicks => true;
-
- [JsonIgnore]
- public string AlbumArtist => AlbumArtists.FirstOrDefault();
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
- /// <summary>
- /// Gets the tracks.
- /// </summary>
- /// <value>The tracks.</value>
- [JsonIgnore]
- public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>();
-
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return Tracks;
@@ -120,7 +122,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 397a68ff7e..f30f8ce7f2 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -6,9 +8,9 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Diacritics.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -42,6 +44,36 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public override bool SupportsPlayedStatus => false;
+ /// <summary>
+ /// Gets the folder containing the item.
+ /// If the item is a folder, it returns the folder itself.
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [JsonIgnore]
+ public override string ContainingFolderPath => Path;
+
+ [JsonIgnore]
+ public override IEnumerable<BaseItem> Children
+ {
+ get
+ {
+ if (IsAccessedByName)
+ {
+ return new List<BaseItem>();
+ }
+
+ return base.Children;
+ }
+ }
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
return 1;
@@ -56,27 +88,13 @@ namespace MediaBrowser.Controller.Entities.Audio
{
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name };
+ query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) };
query.ArtistIds = new[] { Id };
}
return LibraryManager.GetItemList(query);
}
- [JsonIgnore]
- public override IEnumerable<BaseItem> Children
- {
- get
- {
- if (IsAccessedByName)
- {
- return new List<BaseItem>();
- }
-
- return base.Children;
- }
- }
-
public override int GetChildCount(User user)
{
return IsAccessedByName ? 0 : base.GetChildCount(user);
@@ -92,7 +110,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return base.IsSaveLocalMetadataEnabled();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (IsAccessedByName)
{
@@ -100,7 +118,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return Task.CompletedTask;
}
- return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
+ return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken);
}
public override List<string> GetUserDataKeys()
@@ -112,14 +130,6 @@ namespace MediaBrowser.Controller.Entities.Audio
}
/// <summary>
- /// Returns the folder containing the item.
- /// If the item is a folder, it returns the folder itself.
- /// </summary>
- /// <value>The containing folder path.</value>
- [JsonIgnore]
- public override string ContainingFolderPath => Path;
-
- /// <summary>
/// Gets the user data key.
/// </summary>
/// <param name="item">The item.</param>
@@ -145,7 +155,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()
@@ -165,14 +175,6 @@ namespace MediaBrowser.Controller.Entities.Audio
return info;
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
- public static string GetPath(string name)
- {
- return GetPath(name, true);
- }
-
public static string GetPath(string name, bool normalizeName)
{
// Trim the period at the end because windows will have a hard time with that
@@ -206,9 +208,11 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata">Option to replace metadata.</param>
+ /// <returns>True if metadata changed.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (IsAccessedByName)
{
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
index 5a117a6b15..dc6fcc55a5 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -1,9 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Controller.Extensions;
+using Diacritics.Extensions;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities.Audio
@@ -13,19 +15,6 @@ namespace MediaBrowser.Controller.Entities.Audio
/// </summary>
public class MusicGenre : BaseItem, IItemByName
{
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
- return list;
- }
-
- public override string CreatePresentationUniqueKey()
- {
- return GetUserDataKeys()[0];
- }
-
[JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
@@ -36,13 +25,29 @@ namespace MediaBrowser.Controller.Entities.Audio
public override bool IsDisplayedAsFolder => true;
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
[JsonIgnore]
public override string ContainingFolderPath => Path;
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
return 1;
@@ -58,13 +63,10 @@ namespace MediaBrowser.Controller.Entities.Audio
return true;
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
- query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+ query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) };
return LibraryManager.GetItemList(query);
}
@@ -104,9 +106,11 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata">Option to replace metadata.</param>
+ /// <returns>True if metadata changed.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs
index f4bd851e1b..782481fbcd 100644
--- a/MediaBrowser.Controller/Entities/AudioBook.cs
+++ b/MediaBrowser.Controller/Entities/AudioBook.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1724, CS1591
using System;
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 24978d8dde..067fecd878 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS1591, SA1401
using System;
using System.Collections.Generic;
@@ -9,13 +11,14 @@ using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Diacritics.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
@@ -38,6 +41,22 @@ namespace MediaBrowser.Controller.Entities
public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
{
/// <summary>
+ /// The trailer folder name.
+ /// </summary>
+ public const string TrailerFolderName = "trailers";
+ public const string ThemeSongsFolderName = "theme-music";
+ public const string ThemeSongFilename = "theme";
+ public const string ThemeVideosFolderName = "backdrops";
+ public const string ExtrasFolderName = "extras";
+ public const string BehindTheScenesFolderName = "behind the scenes";
+ public const string DeletedScenesFolderName = "deleted scenes";
+ public const string InterviewFolderName = "interviews";
+ public const string SceneFolderName = "scenes";
+ public const string SampleFolderName = "samples";
+ public const string ShortsFolderName = "shorts";
+ public const string FeaturettesFolderName = "featurettes";
+
+ /// <summary>
/// The supported image extensions.
/// </summary>
public static readonly string[] SupportedImageExtensions
@@ -58,10 +77,45 @@ namespace MediaBrowser.Controller.Entities
".ttml"
};
+ /// <summary>
+ /// Extra types that should be counted and displayed as "Special Features" in the UI.
+ /// </summary>
+ public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
+ {
+ Model.Entities.ExtraType.Unknown,
+ Model.Entities.ExtraType.BehindTheScenes,
+ Model.Entities.ExtraType.Clip,
+ Model.Entities.ExtraType.DeletedScene,
+ Model.Entities.ExtraType.Interview,
+ Model.Entities.ExtraType.Sample,
+ Model.Entities.ExtraType.Scene
+ };
+
+ public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
+ public static readonly string[] AllExtrasTypesFolderNames =
+ {
+ ExtrasFolderName,
+ BehindTheScenesFolderName,
+ DeletedScenesFolderName,
+ InterviewFolderName,
+ SceneFolderName,
+ SampleFolderName,
+ ShortsFolderName,
+ FeaturettesFolderName
+ };
+
+ private string _sortName;
+ private Guid[] _themeSongIds;
+ private Guid[] _themeVideoIds;
+
+ private string _forcedSortName;
+
+ private string _name;
+
+ public static char SlugChar = '-';
+
protected BaseItem()
{
- ThemeSongIds = Array.Empty<Guid>();
- ThemeVideoIds = Array.Empty<Guid>();
Tags = Array.Empty<string>();
Genres = Array.Empty<string>();
Studios = Array.Empty<string>();
@@ -73,39 +127,43 @@ namespace MediaBrowser.Controller.Entities
ExtraIds = Array.Empty<Guid>();
}
- public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
- public static char SlugChar = '-';
-
- /// <summary>
- /// The trailer folder name.
- /// </summary>
- public const string TrailerFolderName = "trailers";
- public const string ThemeSongsFolderName = "theme-music";
- public const string ThemeSongFilename = "theme";
- public const string ThemeVideosFolderName = "backdrops";
- public const string ExtrasFolderName = "extras";
- public const string BehindTheScenesFolderName = "behind the scenes";
- public const string DeletedScenesFolderName = "deleted scenes";
- public const string InterviewFolderName = "interviews";
- public const string SceneFolderName = "scenes";
- public const string SampleFolderName = "samples";
+ [JsonIgnore]
+ public Guid[] ThemeSongIds
+ {
+ get
+ {
+ return _themeSongIds ??= GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong)
+ .Select(song => song.Id)
+ .ToArray();
+ }
- public static readonly string[] AllExtrasTypesFolderNames = {
- ExtrasFolderName,
- BehindTheScenesFolderName,
- DeletedScenesFolderName,
- InterviewFolderName,
- SceneFolderName,
- SampleFolderName
- };
+ private set
+ {
+ _themeSongIds = value;
+ }
+ }
[JsonIgnore]
- public Guid[] ThemeSongIds { get; set; }
- [JsonIgnore]
- public Guid[] ThemeVideoIds { get; set; }
+ public Guid[] ThemeVideoIds
+ {
+ get
+ {
+ return _themeVideoIds ??= GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo)
+ .Select(song => song.Id)
+ .ToArray();
+ }
+
+ private set
+ {
+ _themeVideoIds = value;
+ }
+ }
[JsonIgnore]
public string PreferredMetadataCountryCode { get; set; }
+
[JsonIgnore]
public string PreferredMetadataLanguage { get; set; }
@@ -143,7 +201,7 @@ namespace MediaBrowser.Controller.Entities
public virtual bool AlwaysScanInternalMetadataPath => false;
/// <summary>
- /// Gets a value indicating whether this instance is in mixed folder.
+ /// Gets or sets a value indicating whether this instance is in mixed folder.
/// </summary>
/// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
[JsonIgnore]
@@ -158,7 +216,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual bool SupportsRemoteImageDownloading => true;
- private string _name;
/// <summary>
/// Gets or sets the name.
/// </summary>
@@ -209,7 +266,7 @@ namespace MediaBrowser.Controller.Entities
public ProgramAudio? Audio { get; set; }
/// <summary>
- /// Return the id that should be used to key display prefs for this item.
+ /// Gets the id that should be used to key display prefs for this item.
/// Default is based on the type for everything except actual generic folders.
/// </summary>
/// <value>The display prefs id.</value>
@@ -245,7 +302,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
[JsonIgnore]
@@ -270,8 +327,11 @@ namespace MediaBrowser.Controller.Entities
public string ServiceName { get; set; }
/// <summary>
- /// If this content came from an external service, the id of the content on that service.
+ /// Gets or sets the external id.
/// </summary>
+ /// <remarks>
+ /// If this content came from an external service, the id of the content on that service.
+ /// </remarks>
[JsonIgnore]
public string ExternalId { get; set; }
@@ -288,14 +348,8 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual bool IsHidden => false;
- public BaseItem GetOwner()
- {
- var ownerId = OwnerId;
- return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId);
- }
-
/// <summary>
- /// Gets or sets the type of the location.
+ /// Gets the type of the location.
/// </summary>
/// <value>The type of the location.</value>
[JsonIgnore]
@@ -304,9 +358,9 @@ namespace MediaBrowser.Controller.Entities
get
{
// if (IsOffline)
- //{
+ // {
// return LocationType.Offline;
- //}
+ // }
var path = Path;
if (string.IsNullOrEmpty(path))
@@ -339,13 +393,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- public bool IsPathProtocol(MediaProtocol protocol)
- {
- var current = PathProtocol;
-
- return current.HasValue && current.Value == protocol;
- }
-
[JsonIgnore]
public bool IsFileProtocol => IsPathProtocol(MediaProtocol.File);
@@ -383,163 +430,28 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual bool EnableAlphaNumericSorting => true;
- private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
- {
- var list = new List<Tuple<StringBuilder, bool>>();
-
- int thisMarker = 0;
-
- while (thisMarker < s1.Length)
- {
- char thisCh = s1[thisMarker];
+ public virtual bool IsHD => Height >= 720;
- var thisChunk = new StringBuilder();
- bool isNumeric = char.IsDigit(thisCh);
+ public bool IsShortcut { get; set; }
- while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric)
- {
- thisChunk.Append(thisCh);
- thisMarker++;
+ public string ShortcutPath { get; set; }
- if (thisMarker < s1.Length)
- {
- thisCh = s1[thisMarker];
- }
- }
+ public int Width { get; set; }
- list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric));
- }
+ public int Height { get; set; }
- return list;
- }
+ public Guid[] ExtraIds { get; set; }
/// <summary>
- /// This is just a helper for convenience.
+ /// Gets the primary image path.
/// </summary>
+ /// <remarks>
+ /// This is just a helper for convenience.
+ /// </remarks>
/// <value>The primary image path.</value>
[JsonIgnore]
public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
- public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name)
- {
- if (SourceType == SourceType.Channel)
- {
- // hack alert
- return !EnableMediaSourceDisplay;
- }
-
- var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
- if (typeOptions != null)
- {
- return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
- }
-
- if (!libraryOptions.EnableInternetProviders)
- {
- return false;
- }
-
- var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
-
- return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
- }
-
- public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name)
- {
- if (this is Channel)
- {
- // hack alert
- return true;
- }
-
- if (SourceType == SourceType.Channel)
- {
- // hack alert
- return !EnableMediaSourceDisplay;
- }
-
- var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
- if (typeOptions != null)
- {
- return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
- }
-
- if (!libraryOptions.EnableInternetProviders)
- {
- return false;
- }
-
- var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
-
- return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
- }
-
- public virtual bool CanDelete()
- {
- if (SourceType == SourceType.Channel)
- {
- return ChannelManager.CanDelete(this);
- }
-
- return IsFileProtocol;
- }
-
- public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
- {
- if (user.HasPermission(PermissionKind.EnableContentDeletion))
- {
- return true;
- }
-
- var allowed = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders);
-
- if (SourceType == SourceType.Channel)
- {
- return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase);
- }
- else
- {
- var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
-
- foreach (var folder in collectionFolders)
- {
- if (allowed.Contains(folder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- }
-
- return false;
- }
-
- public bool CanDelete(User user, List<Folder> allCollectionFolders)
- {
- return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
- }
-
- public bool CanDelete(User user)
- {
- var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
-
- return CanDelete(user, allCollectionFolders);
- }
-
- public virtual bool CanDownload()
- {
- return false;
- }
-
- public virtual bool IsAuthorizedToDownload(User user)
- {
- return user.HasPermission(PermissionKind.EnableContentDownloading);
- }
-
- public bool CanDownload(User user)
- {
- return CanDownload() && IsAuthorizedToDownload(user);
- }
-
/// <summary>
/// Gets or sets the date created.
/// </summary>
@@ -559,38 +471,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public DateTime DateLastRefreshed { get; set; }
- /// <summary>
- /// The logger.
- /// </summary>
- public static ILogger<BaseItem> Logger { get; set; }
-
- public static ILibraryManager LibraryManager { get; set; }
-
- public static IServerConfigurationManager ConfigurationManager { get; set; }
-
- public static IProviderManager ProviderManager { get; set; }
-
- public static ILocalizationManager LocalizationManager { get; set; }
-
- public static IItemRepository ItemRepository { get; set; }
-
- public static IFileSystem FileSystem { get; set; }
-
- public static IUserDataManager UserDataManager { get; set; }
-
- public static IChannelManager ChannelManager { get; set; }
-
- public static IMediaSourceManager MediaSourceManager { get; set; }
-
- /// <summary>
- /// Returns a <see cref="string" /> that represents this instance.
- /// </summary>
- /// <returns>A <see cref="string" /> that represents this instance.</returns>
- public override string ToString()
- {
- return Name;
- }
-
[JsonIgnore]
public bool IsLocked { get; set; }
@@ -622,219 +502,87 @@ namespace MediaBrowser.Controller.Entities
}
}
- private string _forcedSortName;
- /// <summary>
- /// Gets or sets the name of the forced sort.
- /// </summary>
- /// <value>The name of the forced sort.</value>
[JsonIgnore]
- public string ForcedSortName
- {
- get => _forcedSortName;
- set { _forcedSortName = value; _sortName = null; }
- }
-
- private string _sortName;
- /// <summary>
- /// Gets the name of the sort.
- /// </summary>
- /// <value>The name of the sort.</value>
- [JsonIgnore]
- public string SortName
+ public bool EnableMediaSourceDisplay
{
get
{
- if (_sortName == null)
+ if (SourceType == SourceType.Channel)
{
- if (!string.IsNullOrEmpty(ForcedSortName))
- {
- // Need the ToLower because that's what CreateSortName does
- _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
- }
- else
- {
- _sortName = CreateSortName();
- }
+ return ChannelManager.EnableMediaSourceDisplay(this);
}
- return _sortName;
+ return true;
}
-
- set => _sortName = value;
}
- public string GetInternalMetadataPath()
- {
- var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
-
- return GetInternalMetadataPath(basePath);
- }
-
- protected virtual string GetInternalMetadataPath(string basePath)
- {
- if (SourceType == SourceType.Channel)
- {
- return System.IO.Path.Combine(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);
- }
+ [JsonIgnore]
+ public Guid ParentId { get; set; }
/// <summary>
- /// Creates the name of the sort.
+ /// Gets or sets the logger.
/// </summary>
- /// <returns>System.String.</returns>
- protected virtual string CreateSortName()
- {
- if (Name == null)
- {
- return null; // some items may not have name filled in properly
- }
-
- if (!EnableAlphaNumericSorting)
- {
- return Name.TrimStart();
- }
-
- var sortable = Name.Trim().ToLowerInvariant();
-
- foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
- {
- sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
- }
-
- foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
- {
- sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
- }
-
- foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
- {
- // Remove from beginning if a space follows
- if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
- {
- sortable = sortable.Remove(0, search.Length + 1);
- }
-
- // Remove from middle if surrounded by spaces
- sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
-
- // Remove from end if followed by a space
- if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
- {
- sortable = sortable.Remove(sortable.Length - (search.Length + 1));
- }
- }
-
- return ModifySortChunks(sortable);
- }
+ public static ILogger<BaseItem> Logger { get; set; }
- private string ModifySortChunks(string name)
- {
- var chunks = GetSortChunks(name);
+ public static ILibraryManager LibraryManager { get; set; }
- var builder = new StringBuilder();
+ public static IServerConfigurationManager ConfigurationManager { get; set; }
- foreach (var chunk in chunks)
- {
- var chunkBuilder = chunk.Item1;
+ public static IProviderManager ProviderManager { get; set; }
- // This chunk is numeric
- if (chunk.Item2)
- {
- while (chunkBuilder.Length < 10)
- {
- chunkBuilder.Insert(0, '0');
- }
- }
+ public static ILocalizationManager LocalizationManager { get; set; }
- builder.Append(chunkBuilder);
- }
+ public static IItemRepository ItemRepository { get; set; }
- // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
- return builder.ToString().RemoveDiacritics();
- }
+ public static IFileSystem FileSystem { get; set; }
- [JsonIgnore]
- public bool EnableMediaSourceDisplay
- {
- get
- {
- if (SourceType == SourceType.Channel)
- {
- return ChannelManager.EnableMediaSourceDisplay(this);
- }
+ public static IUserDataManager UserDataManager { get; set; }
- return true;
- }
- }
+ public static IChannelManager ChannelManager { get; set; }
- [JsonIgnore]
- public Guid ParentId { get; set; }
+ public static IMediaSourceManager MediaSourceManager { get; set; }
/// <summary>
- /// Gets or sets the parent.
+ /// Gets or sets the name of the forced sort.
/// </summary>
- /// <value>The parent.</value>
+ /// <value>The name of the forced sort.</value>
[JsonIgnore]
- public Folder Parent
+ public string ForcedSortName
{
- get => GetParent() as Folder;
+ get => _forcedSortName;
set
{
- }
- }
-
- public void SetParent(Folder parent)
- {
- ParentId = parent == null ? Guid.Empty : parent.Id;
- }
-
- public BaseItem GetParent()
- {
- var parentId = ParentId;
- if (!parentId.Equals(Guid.Empty))
- {
- return LibraryManager.GetItemById(parentId);
- }
-
- return null;
- }
-
- public IEnumerable<BaseItem> GetParents()
- {
- var parent = GetParent();
-
- while (parent != null)
- {
- yield return parent;
-
- parent = parent.GetParent();
+ _forcedSortName = value;
+ _sortName = null;
}
}
/// <summary>
- /// Finds a parent of a given type.
+ /// Gets or sets the name of the sort.
/// </summary>
- /// <typeparam name="T"></typeparam>
- /// <returns>``0.</returns>
- public T FindParent<T>()
- where T : Folder
+ /// <value>The name of the sort.</value>
+ [JsonIgnore]
+ public string SortName
{
- foreach (var parent in GetParents())
+ get
{
- var item = parent as T;
- if (item != null)
+ if (_sortName == null)
{
- return item;
+ if (!string.IsNullOrEmpty(ForcedSortName))
+ {
+ // Need the ToLower because that's what CreateSortName does
+ _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant();
+ }
+ else
+ {
+ _sortName = CreateSortName();
+ }
}
+
+ return _sortName;
}
- return null;
+ set => _sortName = value;
}
[JsonIgnore]
@@ -863,7 +611,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
+ /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be first aired.
/// </summary>
/// <value>The premiere date.</value>
[JsonIgnore]
@@ -960,7 +708,7 @@ namespace MediaBrowser.Controller.Entities
public int? ProductionYear { get; set; }
/// <summary>
- /// If the item is part of a series, this is it's number in the series.
+ /// Gets or sets the index number. If the item is part of a series, this is it's number in the series.
/// This could be episode number, album track number, etc.
/// </summary>
/// <value>The index number.</value>
@@ -968,7 +716,7 @@ namespace MediaBrowser.Controller.Entities
public int? IndexNumber { get; set; }
/// <summary>
- /// For an episode this could be the season number, or for a song this could be the disc number.
+ /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this could be the disc number.
/// </summary>
/// <value>The parent index number.</value>
[JsonIgnore]
@@ -1020,6 +768,349 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
+ /// Gets or sets the provider ids.
+ /// </summary>
+ /// <value>The provider ids.</value>
+ [JsonIgnore]
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ [JsonIgnore]
+ public virtual Folder LatestItemsIndexContainer => null;
+
+ [JsonIgnore]
+ public string PresentationUniqueKey { get; set; }
+
+ [JsonIgnore]
+ public virtual bool EnableRememberingTrackSelections => true;
+
+ [JsonIgnore]
+ public virtual bool IsTopParent
+ {
+ get
+ {
+ if (this is BasePluginFolder || this is Channel)
+ {
+ return true;
+ }
+
+ if (this is IHasCollectionType view)
+ {
+ if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ if (GetParent() is AggregateFolder)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ [JsonIgnore]
+ public virtual bool SupportsAncestors => true;
+
+ [JsonIgnore]
+ public virtual bool StopRefreshIfLocalMetadataFound => true;
+
+ [JsonIgnore]
+ protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol;
+
+ [JsonIgnore]
+ public virtual bool SupportsPeople => false;
+
+ [JsonIgnore]
+ public virtual bool SupportsThemeMedia => false;
+
+ [JsonIgnore]
+ public virtual bool SupportsInheritedParentImages => false;
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+ [JsonIgnore]
+ public virtual bool IsFolder => false;
+
+ [JsonIgnore]
+ public virtual bool IsDisplayedAsFolder => false;
+
+ /// <summary>
+ /// Gets or sets the remote trailers.
+ /// </summary>
+ /// <value>The remote trailers.</value>
+ public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
+
+ public virtual bool SupportsExternalTransfer => false;
+
+ public virtual double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 0;
+ }
+
+ public virtual string CreatePresentationUniqueKey()
+ {
+ return Id.ToString("N", CultureInfo.InvariantCulture);
+ }
+
+ public bool IsPathProtocol(MediaProtocol protocol)
+ {
+ var current = PathProtocol;
+
+ return current.HasValue && current.Value == protocol;
+ }
+
+ private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
+ {
+ var list = new List<Tuple<StringBuilder, bool>>();
+
+ int thisMarker = 0;
+
+ while (thisMarker < s1.Length)
+ {
+ char thisCh = s1[thisMarker];
+
+ var thisChunk = new StringBuilder();
+ bool isNumeric = char.IsDigit(thisCh);
+
+ while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric)
+ {
+ thisChunk.Append(thisCh);
+ thisMarker++;
+
+ if (thisMarker < s1.Length)
+ {
+ thisCh = s1[thisMarker];
+ }
+ }
+
+ list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric));
+ }
+
+ return list;
+ }
+
+ public virtual bool CanDelete()
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return ChannelManager.CanDelete(this);
+ }
+
+ return IsFileProtocol;
+ }
+
+ public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
+ {
+ if (user.HasPermission(PermissionKind.EnableContentDeletion))
+ {
+ return true;
+ }
+
+ var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
+
+ if (SourceType == SourceType.Channel)
+ {
+ return allowed.Contains(ChannelId);
+ }
+ else
+ {
+ var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
+
+ foreach (var folder in collectionFolders)
+ {
+ if (allowed.Contains(folder.Id))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public BaseItem GetOwner()
+ {
+ var ownerId = OwnerId;
+ return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId);
+ }
+
+ public bool CanDelete(User user, List<Folder> allCollectionFolders)
+ {
+ return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
+ }
+
+ public bool CanDelete(User user)
+ {
+ var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
+
+ return CanDelete(user, allCollectionFolders);
+ }
+
+ public virtual bool CanDownload()
+ {
+ return false;
+ }
+
+ public virtual bool IsAuthorizedToDownload(User user)
+ {
+ return user.HasPermission(PermissionKind.EnableContentDownloading);
+ }
+
+ public bool CanDownload(User user)
+ {
+ return CanDownload() && IsAuthorizedToDownload(user);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this instance.
+ /// </summary>
+ /// <returns>A <see cref="string" /> that represents this instance.</returns>
+ public override string ToString()
+ {
+ return Name;
+ }
+
+ public string GetInternalMetadataPath()
+ {
+ var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
+
+ return GetInternalMetadataPath(basePath);
+ }
+
+ protected virtual string GetInternalMetadataPath(string basePath)
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ 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);
+
+ return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString);
+ }
+
+ /// <summary>
+ /// Creates the name of the sort.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ protected virtual string CreateSortName()
+ {
+ if (Name == null)
+ {
+ return null; // some items may not have name filled in properly
+ }
+
+ if (!EnableAlphaNumericSorting)
+ {
+ return Name.TrimStart();
+ }
+
+ var sortable = Name.Trim().ToLowerInvariant();
+
+ foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
+ {
+ sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
+ }
+
+ foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
+ {
+ sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
+ }
+
+ foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
+ {
+ // Remove from beginning if a space follows
+ if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
+ {
+ sortable = sortable.Remove(0, search.Length + 1);
+ }
+
+ // Remove from middle if surrounded by spaces
+ sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
+
+ // Remove from end if followed by a space
+ if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
+ {
+ sortable = sortable.Remove(sortable.Length - (search.Length + 1));
+ }
+ }
+
+ return ModifySortChunks(sortable);
+ }
+
+ private string ModifySortChunks(string name)
+ {
+ var chunks = GetSortChunks(name);
+
+ var builder = new StringBuilder();
+
+ foreach (var chunk in chunks)
+ {
+ var chunkBuilder = chunk.Item1;
+
+ // This chunk is numeric
+ if (chunk.Item2)
+ {
+ while (chunkBuilder.Length < 10)
+ {
+ chunkBuilder.Insert(0, '0');
+ }
+ }
+
+ builder.Append(chunkBuilder);
+ }
+
+ // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
+ return builder.ToString().RemoveDiacritics();
+ }
+
+ public BaseItem GetParent()
+ {
+ var parentId = ParentId;
+ if (!parentId.Equals(Guid.Empty))
+ {
+ return LibraryManager.GetItemById(parentId);
+ }
+
+ return null;
+ }
+
+ public IEnumerable<BaseItem> GetParents()
+ {
+ var parent = GetParent();
+
+ while (parent != null)
+ {
+ yield return parent;
+
+ parent = parent.GetParent();
+ }
+ }
+
+ /// <summary>
+ /// Finds a parent of a given type.
+ /// </summary>
+ /// <typeparam name="T">Type of parent.</typeparam>
+ /// <returns>``0.</returns>
+ public T FindParent<T>()
+ where T : Folder
+ {
+ foreach (var parent in GetParents())
+ {
+ if (parent is T item)
+ {
+ return item;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
/// Gets the play access.
/// </summary>
/// <param name="user">The user.</param>
@@ -1032,9 +1123,9 @@ namespace MediaBrowser.Controller.Entities
}
// if (!user.IsParentalScheduleAllowed())
- //{
+ // {
// return PlayAccess.None;
- //}
+ // }
return PlayAccess.Full;
}
@@ -1250,7 +1341,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- return string.Join("/", terms.ToArray());
+ return string.Join('/', terms.ToArray());
}
/// <summary>
@@ -1266,7 +1357,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>()
@@ -1327,14 +1418,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 =>
{
@@ -1345,7 +1438,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;
@@ -1422,23 +1515,55 @@ namespace MediaBrowser.Controller.Entities
}
}
- [JsonIgnore]
- protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol;
+ protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
+ {
+ if (!IsVisible(user))
+ {
+ return false;
+ }
- [JsonIgnore]
- public virtual bool SupportsPeople => false;
+ if (GetParents().Any(i => !i.IsVisible(user)))
+ {
+ return false;
+ }
- [JsonIgnore]
- public virtual bool SupportsThemeMedia => false;
+ if (checkFolders)
+ {
+ var topParent = GetParents().LastOrDefault() ?? this;
+
+ if (string.IsNullOrEmpty(topParent.Path))
+ {
+ return true;
+ }
+
+ var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
+
+ if (itemCollectionFolders.Count > 0)
+ {
+ var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList();
+ if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public void SetParent(Folder parent)
+ {
+ ParentId = parent == null ? Guid.Empty : parent.Id;
+ }
/// <summary>
/// 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;
@@ -1582,7 +1707,8 @@ namespace MediaBrowser.Controller.Entities
await Task.WhenAll(tasks).ConfigureAwait(false);
- item.ThemeVideoIds = newThemeVideoIds;
+ // They are expected to be sorted by SortName
+ item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
return themeVideosChanged;
}
@@ -1619,34 +1745,12 @@ namespace MediaBrowser.Controller.Entities
await Task.WhenAll(tasks).ConfigureAwait(false);
- item.ThemeSongIds = newThemeSongIds;
+ // They are expected to be sorted by SortName
+ item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
return themeSongsChanged;
}
- /// <summary>
- /// Gets or sets the provider ids.
- /// </summary>
- /// <value>The provider ids.</value>
- [JsonIgnore]
- public Dictionary<string, string> ProviderIds { get; set; }
-
- [JsonIgnore]
- public virtual Folder LatestItemsIndexContainer => null;
-
- public virtual double GetDefaultPrimaryImageAspectRatio()
- {
- return 0;
- }
-
- public virtual string CreatePresentationUniqueKey()
- {
- return Id.ToString("N", CultureInfo.InvariantCulture);
- }
-
- [JsonIgnore]
- public string PresentationUniqueKey { get; set; }
-
public string GetPresentationUniqueKey()
{
return PresentationUniqueKey ?? CreatePresentationUniqueKey();
@@ -1778,7 +1882,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)
@@ -1914,7 +2018,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType().ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType());
}
/// <summary>
@@ -1923,7 +2027,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)
@@ -1944,55 +2048,6 @@ namespace MediaBrowser.Controller.Entities
return IsVisibleStandaloneInternal(user, true);
}
- [JsonIgnore]
- public virtual bool SupportsInheritedParentImages => false;
-
- protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
- {
- if (!IsVisible(user))
- {
- return false;
- }
-
- if (GetParents().Any(i => !i.IsVisible(user)))
- {
- return false;
- }
-
- if (checkFolders)
- {
- var topParent = GetParents().LastOrDefault() ?? this;
-
- if (string.IsNullOrEmpty(topParent.Path))
- {
- return true;
- }
-
- var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
-
- if (itemCollectionFolders.Count > 0)
- {
- var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList();
- if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
- {
- return false;
- }
- }
- }
-
- return true;
- }
-
- /// <summary>
- /// Gets a value indicating whether this instance is folder.
- /// </summary>
- /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public virtual bool IsFolder => false;
-
- [JsonIgnore]
- public virtual bool IsDisplayedAsFolder => false;
-
public virtual string GetClientTypeName()
{
if (IsFolder && SourceType == SourceType.Channel && !(this is Channel))
@@ -2003,6 +2058,11 @@ namespace MediaBrowser.Controller.Entities
return GetType().Name;
}
+ public BaseItemKind GetBaseItemKind()
+ {
+ return Enum.Parse<BaseItemKind>(GetClientTypeName());
+ }
+
/// <summary>
/// Gets the linked child.
/// </summary>
@@ -2076,14 +2136,11 @@ namespace MediaBrowser.Controller.Entities
return null;
}
- [JsonIgnore]
- public virtual bool EnableRememberingTrackSelections => true;
-
/// <summary>
/// Adds a studio to the item.
/// </summary>
/// <param name="name">The name.</param>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
public void AddStudio(string name)
{
if (string.IsNullOrEmpty(name))
@@ -2119,7 +2176,7 @@ namespace MediaBrowser.Controller.Entities
/// Adds a genre to the item.
/// </summary>
/// <param name="name">The name.</param>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throwns if name is null.</exception>
public void AddGenre(string name)
{
if (string.IsNullOrEmpty(name))
@@ -2142,8 +2199,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="user">The user.</param>
/// <param name="datePlayed">The date played.</param>
/// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
public virtual void MarkPlayed(
User user,
DateTime? datePlayed,
@@ -2180,8 +2236,7 @@ namespace MediaBrowser.Controller.Entities
/// Marks the unplayed.
/// </summary>
/// <param name="user">The user.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
public virtual void MarkUnplayed(User user)
{
if (user == null)
@@ -2216,7 +2271,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;
@@ -2281,6 +2336,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="type">The type.</param>
/// <param name="index">The index.</param>
+ /// <returns>A task.</returns>
public async Task DeleteImageAsync(ImageType type, int index)
{
var info = GetImageInfo(type, index);
@@ -2318,13 +2374,15 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Validates that images within the item are still on the filesystem.
/// </summary>
+ /// <param name="directoryService">The directory service to use.</param>
+ /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
public bool ValidateImages(IDirectoryService directoryService)
{
var allFiles = ImageInfos
.Where(i => i.IsLocalFile)
.Select(i => System.IO.Path.GetDirectoryName(i.Path))
.Distinct(StringComparer.OrdinalIgnoreCase)
- .SelectMany(i => directoryService.GetFilePaths(i))
+ .SelectMany(path => directoryService.GetFilePaths(path))
.ToList();
var deletedImages = ImageInfos
@@ -2345,9 +2403,7 @@ 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="ArgumentNullException">Item is null.</exception>
public string GetImagePath(ImageType imageType, int imageIndex)
=> GetImageInfo(imageType, imageIndex)?.Path;
@@ -2434,7 +2490,15 @@ namespace MediaBrowser.Controller.Entities
throw new ArgumentException("No image info for chapter images");
}
- return ImageInfos.Where(i => i.Type == imageType);
+ // Yield return is more performant than LINQ Where on an Array
+ for (var i = 0; i < ImageInfos.Length; i++)
+ {
+ var imageInfo = ImageInfos[i];
+ if (imageInfo.Type == imageType)
+ {
+ yield return imageInfo;
+ }
+ }
}
/// <summary>
@@ -2443,7 +2507,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="images">The images.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
- /// <exception cref="ArgumentException">Cannot call AddImages with chapter images</exception>
+ /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
{
if (imageType == ImageType.Chapter)
@@ -2466,7 +2530,7 @@ namespace MediaBrowser.Controller.Entities
}
var existing = existingImages
- .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
+ .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
@@ -2497,8 +2561,7 @@ namespace MediaBrowser.Controller.Entities
var newImagePaths = images.Select(i => i.FullName).ToList();
var deleted = existingImages
- .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path))
- .ToList();
+ .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path));
if (deleted.Count > 0)
{
@@ -2527,10 +2590,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,
@@ -2562,7 +2626,7 @@ namespace MediaBrowser.Controller.Entities
{
if (!AllowsMultipleImages(type))
{
- throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots");
+ throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots");
}
var info1 = GetImageInfo(type, index1);
@@ -2633,9 +2697,11 @@ namespace MediaBrowser.Controller.Entities
{
return new T
{
+ Path = Path,
MetadataCountryCode = GetPreferredMetadataCountryCode(),
MetadataLanguage = GetPreferredMetadataLanguage(),
Name = GetNameForMetadataLookup(),
+ OriginalTitle = OriginalTitle,
ProviderIds = ProviderIds,
IndexNumber = IndexNumber,
ParentIndexNumber = ParentIndexNumber,
@@ -2652,7 +2718,9 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is called before any metadata refresh and returns true if changes were made.
/// </summary>
- public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
+ /// <returns>true if the item has change, else false.</returns>
+ public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
_sortName = null;
@@ -2776,11 +2844,11 @@ namespace MediaBrowser.Controller.Entities
// var parentId = Id;
// if (!video.IsOwnedItem || video.ParentId != parentId)
- //{
+ // {
// video.IsOwnedItem = true;
// video.ParentId = parentId;
// newOptions.ForceSave = true;
- //}
+ // }
if (video == null)
{
@@ -2794,7 +2862,7 @@ namespace MediaBrowser.Controller.Entities
{
var list = GetEtagValues(user);
- return string.Join("|", list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
protected virtual List<string> GetEtagValues(User user)
@@ -2820,39 +2888,6 @@ namespace MediaBrowser.Controller.Entities
return GetParents().FirstOrDefault(parent => parent.IsTopParent);
}
- [JsonIgnore]
- public virtual bool IsTopParent
- {
- get
- {
- if (this is BasePluginFolder || this is Channel)
- {
- return true;
- }
-
- if (this is IHasCollectionType view)
- {
- if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- if (GetParent() is AggregateFolder)
- {
- return true;
- }
-
- return false;
- }
- }
-
- [JsonIgnore]
- public virtual bool SupportsAncestors => true;
-
- [JsonIgnore]
- public virtual bool StopRefreshIfLocalMetadataFound => true;
-
public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
{
return new[] { Id };
@@ -2887,7 +2922,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Updates the official rating based on content and returns true or false indicating if it changed.
/// </summary>
- /// <returns></returns>
+ /// <param name="children">Media children.</param>
+ /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
public bool UpdateRatingToItems(IList<BaseItem> children)
{
var currentOfficialRating = OfficialRating;
@@ -2903,27 +2939,23 @@ 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);
}
public IEnumerable<BaseItem> GetThemeSongs()
{
- return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName);
+ return ThemeSongIds.Select(LibraryManager.GetItemById);
}
public IEnumerable<BaseItem> GetThemeVideos()
{
- return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName);
+ return ThemeVideoIds.Select(LibraryManager.GetItemById);
}
/// <summary>
- /// Gets or sets the remote trailers.
- /// </summary>
- /// <value>The remote trailers.</value>
- public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
-
- /// <summary>
/// Get all extras associated with this item, sorted by <see cref="SortName"/>.
/// </summary>
/// <returns>An enumerable containing the items.</returns>
@@ -2960,39 +2992,11 @@ namespace MediaBrowser.Controller.Entities
}
}
- public virtual bool IsHD => Height >= 720;
-
- public bool IsShortcut { get; set; }
-
- public string ShortcutPath { get; set; }
-
- public int Width { get; set; }
-
- public int Height { get; set; }
-
- public Guid[] ExtraIds { get; set; }
-
public virtual long GetRunTimeTicksForPlayState()
{
return RunTimeTicks ?? 0;
}
- /// <summary>
- /// Extra types that should be counted and displayed as "Special Features" in the UI.
- /// </summary>
- public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
- {
- Model.Entities.ExtraType.Unknown,
- Model.Entities.ExtraType.BehindTheScenes,
- Model.Entities.ExtraType.Clip,
- Model.Entities.ExtraType.DeletedScene,
- Model.Entities.ExtraType.Interview,
- Model.Entities.ExtraType.Sample,
- Model.Entities.ExtraType.Scene
- };
-
- public virtual bool SupportsExternalTransfer => false;
-
/// <inheritdoc />
public override bool Equals(object obj)
{
@@ -3000,7 +3004,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 8a69971d0f..e88121212a 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using System.Linq;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -45,7 +46,8 @@ namespace MediaBrowser.Controller.Entities
{
if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase))
{
- item.SetImage(new ItemImageInfo
+ item.SetImage(
+ new ItemImageInfo
{
Path = file,
Type = imageType
@@ -62,10 +64,22 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="source">The source object.</param>
/// <param name="dest">The destination object.</param>
+ /// <typeparam name="T">Source type.</typeparam>
+ /// <typeparam name="TU">Destination type.</typeparam>
public static void DeepCopy<T, TU>(this T source, TU dest)
- where T : BaseItem
- where TU : BaseItem
+ where T : BaseItem
+ where TU : BaseItem
{
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (dest == null)
+ {
+ throw new ArgumentNullException(nameof(dest));
+ }
+
var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList();
foreach (var sourceProp in typeof(T).GetProperties())
@@ -97,9 +111,12 @@ namespace MediaBrowser.Controller.Entities
/// Copies all properties on newly created object. Skips properties that do not exist.
/// </summary>
/// <param name="source">The source object.</param>
+ /// <typeparam name="T">Source type.</typeparam>
+ /// <typeparam name="TU">Destination type.</typeparam>
+ /// <returns>Destination object.</returns>
public static TU DeepCopy<T, TU>(this T source)
- where T : BaseItem
- where TU : BaseItem, new()
+ where T : BaseItem
+ where TU : BaseItem, new()
{
var dest = new TU();
source.DeepCopy(dest);
diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
index ef5a5a734c..272a37df1b 100644
--- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs
+++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Text.Json.Serialization;
@@ -13,6 +15,12 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual string CollectionType => null;
+ [JsonIgnore]
+ public override bool SupportsInheritedParentImages => false;
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
public override bool CanDelete()
{
return false;
@@ -22,11 +30,5 @@ namespace MediaBrowser.Controller.Entities
{
return true;
}
-
- [JsonIgnore]
- public override bool SupportsInheritedParentImages => false;
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs
index 55945283c9..d75beb06da 100644
--- a/MediaBrowser.Controller/Entities/Book.cs
+++ b/MediaBrowser.Controller/Entities/Book.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,6 +12,11 @@ namespace MediaBrowser.Controller.Entities
{
public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries
{
+ public Book()
+ {
+ this.RunTimeTicks = TimeSpan.TicksPerSecond;
+ }
+
[JsonIgnore]
public override string MediaType => Model.Entities.MediaType.Book;
@@ -26,11 +33,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public Guid SeriesId { get; set; }
- public Book()
- {
- this.RunTimeTicks = TimeSpan.TicksPerSecond;
- }
-
public string FindSeriesSortName()
{
return SeriesName;
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index d25545a2fc..0fb4771dd3 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -1,12 +1,16 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -24,32 +28,66 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class CollectionFolder : Folder, ICollectionFolder
{
- public static IXmlSerializer XmlSerializer { get; set; }
-
- public static IJsonSerializer JsonSerializer { get; set; }
-
- public static IServerApplicationHost ApplicationHost { get; set; }
+ private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
+ private bool _requiresRefresh;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CollectionFolder"/> class.
+ /// </summary>
public CollectionFolder()
{
PhysicalLocationsList = Array.Empty<string>();
PhysicalFolderIds = Array.Empty<Guid>();
}
+ /// <summary>
+ /// Gets the display preferences id.
+ /// </summary>
+ /// <remarks>
+ /// Allow different display preferences for each collection folder.
+ /// </remarks>
+ /// <value>The display prefs id.</value>
+ [JsonIgnore]
+ public override Guid DisplayPreferencesId => Id;
+
+ [JsonIgnore]
+ public override string[] PhysicalLocations => PhysicalLocationsList;
+
+ public string[] PhysicalLocationsList { get; set; }
+
+ public Guid[] PhysicalFolderIds { get; set; }
+
+ public static IXmlSerializer XmlSerializer { get; set; }
+
+ public static IServerApplicationHost ApplicationHost { get; set; }
+
[JsonIgnore]
public override bool SupportsPlayedStatus => false;
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
+ public string CollectionType { get; set; }
+
+ /// <summary>
+ /// Gets the item's children.
+ /// </summary>
+ /// <remarks>
+ /// Our children are actually just references to the ones in the physical root...
+ /// </remarks>
+ /// <value>The actual children.</value>
+ [JsonIgnore]
+ public override IEnumerable<BaseItem> Children => GetActualChildren();
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
public override bool CanDelete()
{
return false;
}
- public string CollectionType { get; set; }
-
- private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>();
public LibraryOptions GetLibraryOptions()
{
return GetLibraryOptions(Path);
@@ -60,7 +98,6 @@ namespace MediaBrowser.Controller.Entities
try
{
var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions;
-
if (result == null)
{
return new LibraryOptions();
@@ -104,12 +141,12 @@ namespace MediaBrowser.Controller.Entities
public static LibraryOptions GetLibraryOptions(string path)
{
- lock (LibraryOptions)
+ lock (_libraryOptions)
{
- if (!LibraryOptions.TryGetValue(path, out var options))
+ if (!_libraryOptions.TryGetValue(path, out var options))
{
options = LoadLibraryOptions(path);
- LibraryOptions[path] = options;
+ _libraryOptions[path] = options;
}
return options;
@@ -118,11 +155,11 @@ namespace MediaBrowser.Controller.Entities
public static void SaveLibraryOptions(string path, LibraryOptions options)
{
- lock (LibraryOptions)
+ lock (_libraryOptions)
{
- LibraryOptions[path] = options;
+ _libraryOptions[path] = options;
- var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options));
+ var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
@@ -137,37 +174,22 @@ namespace MediaBrowser.Controller.Entities
public static void OnCollectionFolderChange()
{
- lock (LibraryOptions)
+ lock (_libraryOptions)
{
- LibraryOptions.Clear();
+ _libraryOptions.Clear();
}
}
- /// <summary>
- /// Allow different display preferences for each collection folder.
- /// </summary>
- /// <value>The display prefs id.</value>
- [JsonIgnore]
- public override Guid DisplayPreferencesId => Id;
-
- [JsonIgnore]
- public override string[] PhysicalLocations => PhysicalLocationsList;
-
public override bool IsSaveLocalMetadataEnabled()
{
return true;
}
- public string[] PhysicalLocationsList { get; set; }
-
- public Guid[] PhysicalFolderIds { get; set; }
-
protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
{
return CreateResolveArgs(directoryService, true).FileSystemChildren;
}
- private bool _requiresRefresh;
public override bool RequiresRefresh()
{
var changed = base.RequiresRefresh() || _requiresRefresh;
@@ -199,9 +221,9 @@ namespace MediaBrowser.Controller.Entities
return changed;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh;
+ var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh;
_requiresRefresh = false;
return changed;
}
@@ -270,7 +292,6 @@ namespace MediaBrowser.Controller.Entities
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
{
FileInfo = FileSystem.GetDirectoryInfo(path),
- Path = path,
Parent = GetParent() as Folder,
CollectionType = CollectionType
};
@@ -297,27 +318,20 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
- /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+ /// ***Currently does not contain logic to maintain items that are unavailable in the file system***.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
- /// <summary>
- /// Our children are actually just references to the ones in the physical root...
- /// </summary>
- /// <value>The actual children.</value>
- [JsonIgnore]
- public override IEnumerable<BaseItem> Children => GetActualChildren();
-
public IEnumerable<BaseItem> GetActualChildren()
{
return GetPhysicalFolders(true).SelectMany(c => c.Children);
@@ -354,9 +368,7 @@ namespace MediaBrowser.Controller.Entities
if (result.Count == 0)
{
- var folder = LibraryManager.FindByPath(path, true) as Folder;
-
- if (folder != null)
+ if (LibraryManager.FindByPath(path, true) is Folder folder)
{
result.Add(folder);
}
@@ -364,8 +376,5 @@ namespace MediaBrowser.Controller.Entities
return result;
}
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs
index 3a34c668cf..9ce8eebe34 100644
--- a/MediaBrowser.Controller/Entities/Extensions.cs
+++ b/MediaBrowser.Controller/Entities/Extensions.cs
@@ -1,6 +1,8 @@
+#nullable disable
+
using System;
using System.Linq;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@@ -13,6 +15,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the trailer URL.
/// </summary>
+ /// <param name="item">Media item.</param>
+ /// <param name="url">Trailer URL.</param>
public static void AddTrailerUrl(this BaseItem item, string url)
{
if (string.IsNullOrEmpty(url))
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 11542c1cad..d45a02cf2d 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1,13 +1,15 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CA1721, CA1819, CS1591
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Progress;
@@ -35,6 +37,11 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Folder : BaseItem
{
+ public Folder()
+ {
+ LinkedChildren = Array.Empty<LinkedChild>();
+ }
+
public static IUserViewManager UserViewManager { get; set; }
/// <summary>
@@ -48,11 +55,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public DateTime? DateLastMediaAdded { get; set; }
- public Folder()
- {
- LinkedChildren = Array.Empty<LinkedChild>();
- }
-
[JsonIgnore]
public override bool SupportsThemeMedia => true;
@@ -84,6 +86,87 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual bool SupportsDateLastMediaAdded => false;
+ [JsonIgnore]
+ public override string FileNameWithoutExtension
+ {
+ get
+ {
+ if (IsFileProtocol)
+ {
+ return System.IO.Path.GetFileName(Path);
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the actual children.
+ /// </summary>
+ /// <value>The actual children.</value>
+ [JsonIgnore]
+ public virtual IEnumerable<BaseItem> Children => LoadChildren();
+
+ /// <summary>
+ /// Gets thread-safe access to all recursive children of this folder - without regard to user.
+ /// </summary>
+ /// <value>The recursive children.</value>
+ [JsonIgnore]
+ public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
+
+ [JsonIgnore]
+ protected virtual bool SupportsShortcutChildren => false;
+
+ protected virtual bool FilterLinkedChildrenPerUser => false;
+
+ [JsonIgnore]
+ protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
+
+ [JsonIgnore]
+ public virtual bool SupportsUserDataFromChildren
+ {
+ get
+ {
+ // These are just far too slow.
+ if (this is ICollectionFolder)
+ {
+ return false;
+ }
+
+ if (this is UserView)
+ {
+ return false;
+ }
+
+ if (this is UserRootFolder)
+ {
+ return false;
+ }
+
+ if (this is Channel)
+ {
+ return false;
+ }
+
+ if (SourceType != SourceType.Library)
+ {
+ return false;
+ }
+
+ if (this is IItemByName)
+ {
+ if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public static ICollectionManager CollectionManager { get; set; }
+
public override bool CanDelete()
{
if (IsRoot)
@@ -106,20 +189,6 @@ namespace MediaBrowser.Controller.Entities
return baseResult;
}
- [JsonIgnore]
- public override string FileNameWithoutExtension
- {
- get
- {
- if (IsFileProtocol)
- {
- return System.IO.Path.GetFileName(Path);
- }
-
- return null;
- }
- }
-
protected override bool IsAllowTagFilterEnforced()
{
if (this is ICollectionFolder)
@@ -135,17 +204,12 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- [JsonIgnore]
- protected virtual bool SupportsShortcutChildren => false;
-
/// <summary>
/// Adds the child.
/// </summary>
/// <param name="item">The item.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="InvalidOperationException">Unable to add + item.Name</exception>
- public void AddChild(BaseItem item, CancellationToken cancellationToken)
+ /// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception>
+ public void AddChild(BaseItem item)
{
item.SetParent(this);
@@ -167,31 +231,14 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.CreateItem(item, this);
}
- /// <summary>
- /// Gets the actual children.
- /// </summary>
- /// <value>The actual children.</value>
- [JsonIgnore]
- public virtual IEnumerable<BaseItem> Children => LoadChildren();
-
- /// <summary>
- /// thread-safe access to all recursive children of this folder - without regard to user.
- /// </summary>
- /// <value>The recursive children.</value>
- [JsonIgnore]
- public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren();
-
public override bool IsVisible(User user)
{
if (this is ICollectionFolder && !(this is BasePluginFolder))
{
- var blockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders);
+ var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
if (blockedMediaFolders.Length > 0)
{
- if (blockedMediaFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) ||
-
- // Backwards compatibility
- blockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
+ if (blockedMediaFolders.Contains(Id))
{
return false;
}
@@ -199,8 +246,7 @@ namespace MediaBrowser.Controller.Entities
else
{
if (!user.HasPermission(PermissionKind.EnableAllFolders)
- && !user.GetPreference(PreferenceKind.EnabledFolders)
- .Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id))
{
return false;
}
@@ -212,8 +258,9 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Loads our children. Validation will occur externally.
- /// We want this sychronous.
+ /// We want this synchronous.
/// </summary>
+ /// <returns>Returns children.</returns>
protected virtual List<BaseItem> LoadChildren()
{
// logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
@@ -228,20 +275,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()
@@ -255,7 +302,8 @@ namespace MediaBrowser.Controller.Entities
var id = child.Id;
if (dictionary.ContainsKey(id))
{
- Logger.LogError("Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}",
+ Logger.LogError(
+ "Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}",
Path ?? Name,
child.Path ?? child.Name);
}
@@ -280,13 +328,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)
{
@@ -295,7 +343,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
{
@@ -306,7 +354,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();
@@ -327,11 +375,11 @@ namespace MediaBrowser.Controller.Entities
return;
}
- progress.Report(5);
+ progress.Report(ProgressHelpers.RetrievedChildren);
if (recursive)
{
- ProviderManager.OnRefreshProgress(this, 5);
+ ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren);
}
// Build a dictionary of the current children we have now by Id so we can compare quickly and easily
@@ -392,11 +440,11 @@ namespace MediaBrowser.Controller.Entities
validChildrenNeedGeneration = true;
}
- progress.Report(10);
+ progress.Report(ProgressHelpers.UpdatedChildItems);
if (recursive)
{
- ProviderManager.OnRefreshProgress(this, 10);
+ ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems);
}
cancellationToken.ThrowIfCancellationRequested();
@@ -406,11 +454,13 @@ namespace MediaBrowser.Controller.Entities
var innerProgress = new ActionableProgress<double>();
var folder = this;
- innerProgress.RegisterAction(p =>
+ innerProgress.RegisterAction(innerPercent =>
{
- double newPct = 0.80 * p + 10;
- progress.Report(newPct);
- ProviderManager.OnRefreshProgress(folder, newPct);
+ var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.ScannedSubfolders, innerPercent);
+
+ progress.Report(percent);
+
+ ProviderManager.OnRefreshProgress(folder, percent);
});
if (validChildrenNeedGeneration)
@@ -424,11 +474,11 @@ namespace MediaBrowser.Controller.Entities
if (refreshChildMetadata)
{
- progress.Report(90);
+ progress.Report(ProgressHelpers.ScannedSubfolders);
if (recursive)
{
- ProviderManager.OnRefreshProgress(this, 90);
+ ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders);
}
var container = this as IMetadataContainer;
@@ -436,13 +486,15 @@ namespace MediaBrowser.Controller.Entities
var innerProgress = new ActionableProgress<double>();
var folder = this;
- innerProgress.RegisterAction(p =>
+ innerProgress.RegisterAction(innerPercent =>
{
- double newPct = 0.10 * p + 90;
- progress.Report(newPct);
+ var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.RefreshedMetadata, innerPercent);
+
+ progress.Report(percent);
+
if (recursive)
{
- ProviderManager.OnRefreshProgress(folder, newPct);
+ ProviderManager.OnRefreshProgress(folder, percent);
}
});
@@ -457,55 +509,35 @@ namespace MediaBrowser.Controller.Entities
validChildren = Children.ToList();
}
- await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken);
+ await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken).ConfigureAwait(false);
}
}
}
- private async Task RefreshMetadataRecursive(List<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
+ private Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
{
- var numComplete = 0;
- var count = children.Count;
- double currentPercent = 0;
-
- foreach (var child in children)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var innerProgress = new ActionableProgress<double>();
-
- // Avoid implicitly captured closure
- var currentInnerPercent = currentPercent;
-
- innerProgress.RegisterAction(p =>
- {
- double innerPercent = currentInnerPercent;
- innerPercent += p / count;
- progress.Report(innerPercent);
- });
-
- await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken)
- .ConfigureAwait(false);
-
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
- currentPercent = percent;
-
- progress.Report(percent);
- }
+ return RunTasks(
+ (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken),
+ children,
+ progress,
+ cancellationToken);
}
private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
- var series = container as Series;
- if (series != null)
- {
- await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
- }
+ // limit the amount of concurrent metadata refreshes
+ await ProviderManager.RunMetadataRefresh(
+ async () =>
+ {
+ var series = container as Series;
+ if (series != null)
+ {
+ await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
- await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
+ await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
+ },
+ cancellationToken).ConfigureAwait(false);
}
private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
@@ -520,12 +552,15 @@ namespace MediaBrowser.Controller.Entities
{
if (refreshOptions.RefreshItem(child))
{
- await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+ // limit the amount of concurrent metadata refreshes
+ await ProviderManager.RunMetadataRefresh(
+ async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false),
+ cancellationToken).ConfigureAwait(false);
}
if (recursive && child is Folder folder)
{
- await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken);
+ await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -538,45 +573,80 @@ namespace MediaBrowser.Controller.Entities
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
+ private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
{
- var numComplete = 0;
- var count = children.Count;
- double currentPercent = 0;
+ return RunTasks(
+ (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, null, directoryService, cancellationToken),
+ children,
+ progress,
+ cancellationToken);
+ }
- foreach (var child in children)
+ /// <summary>
+ /// Runs an action block on a list of children.
+ /// </summary>
+ /// <param name="task">The task to run for each child.</param>
+ /// <param name="children">The list of children.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var childrenCount = children.Count;
+ var childrenProgress = new double[childrenCount];
+
+ void UpdateProgress()
{
- cancellationToken.ThrowIfCancellationRequested();
+ progress.Report(childrenProgress.Average());
+ }
- var innerProgress = new ActionableProgress<double>();
+ var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
+ var parallelism = fanoutConcurrency == 0 ? Environment.ProcessorCount : fanoutConcurrency;
+
+ var actionBlock = new ActionBlock<int>(
+ async i =>
+ {
+ var innerProgress = new ActionableProgress<double>();
+
+ innerProgress.RegisterAction(innerPercent =>
+ {
+ // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls
+ var innerPercentRounded = Math.Round(innerPercent);
+ if (childrenProgress[i] != innerPercentRounded)
+ {
+ childrenProgress[i] = innerPercentRounded;
+ UpdateProgress();
+ }
+ });
+
+ await task(children[i], innerProgress).ConfigureAwait(false);
- // Avoid implicitly captured closure
- var currentInnerPercent = currentPercent;
+ childrenProgress[i] = 100;
- innerProgress.RegisterAction(p =>
+ UpdateProgress();
+ },
+ new ExecutionDataflowBlockOptions
{
- double innerPercent = currentInnerPercent;
- innerPercent += p / count;
- progress.Report(innerPercent);
+ MaxDegreeOfParallelism = parallelism,
+ CancellationToken = cancellationToken,
});
- await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService)
- .ConfigureAwait(false);
+ for (var i = 0; i < childrenCount; i++)
+ {
+ actionBlock.Post(i);
+ }
- numComplete++;
- double percent = numComplete;
- percent /= count;
- percent *= 100;
- currentPercent = percent;
+ actionBlock.Complete();
- progress.Report(percent);
- }
+ await actionBlock.Completion.ConfigureAwait(false);
}
/// <summary>
/// Get the children of this folder from the actual file system.
/// </summary>
/// <returns>IEnumerable{BaseItem}.</returns>
+ /// <param name="directoryService">The directory service to use for operation.</param>
+ /// <returns>Returns set of base items.</returns>
protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
{
var collectionType = LibraryManager.GetContentType(this);
@@ -722,7 +792,7 @@ namespace MediaBrowser.Controller.Entities
private bool RequiresPostFiltering2(InternalItemsQuery query)
{
- if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase))
{
Logger.LogDebug("Query requires post-filtering due to BoxSet query");
return true;
@@ -812,7 +882,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsPlayed.HasValue)
{
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name))
+ if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series)))
{
Logger.LogDebug("Query requires post-filtering due to IsPlayed");
return true;
@@ -921,14 +991,18 @@ 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);
}
- public static ICollectionManager CollectionManager { get; set; }
-
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
{
var user = query.User;
@@ -946,7 +1020,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))
@@ -984,7 +1058,8 @@ namespace MediaBrowser.Controller.Entities
return items;
}
- private static bool CollapseBoxSetItems(InternalItemsQuery query,
+ private static bool CollapseBoxSetItems(
+ InternalItemsQuery query,
BaseItem queryParent,
User user,
IServerConfigurationManager configurationManager)
@@ -1065,12 +1140,12 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.Genres.Length > 0)
+ if (request.Genres.Count > 0)
{
return false;
}
- if (request.GenreIds.Length > 0)
+ if (request.GenreIds.Count > 0)
{
return false;
}
@@ -1175,7 +1250,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.GenreIds.Length > 0)
+ if (request.GenreIds.Count > 0)
{
return false;
}
@@ -1256,10 +1331,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;
@@ -1299,18 +1387,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- /// <summary>
- /// Gets allowed recursive children of an item.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
- /// <returns>IEnumerable{BaseItem}.</returns>
- /// <exception cref="ArgumentNullException"></exception>
- public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
- {
- return GetRecursiveChildren(user, null);
- }
-
public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
if (user == null)
@@ -1386,7 +1462,6 @@ namespace MediaBrowser.Controller.Entities
}
}
-
/// <summary>
/// Gets the linked children.
/// </summary>
@@ -1409,16 +1484,19 @@ namespace MediaBrowser.Controller.Entities
return list;
}
- protected virtual bool FilterLinkedChildrenPerUser => false;
-
public bool ContainsLinkedChildByItemId(Guid itemId)
{
var linkedChildren = LinkedChildren;
foreach (var i in linkedChildren)
{
- if (i.ItemId.HasValue && i.ItemId.Value == itemId)
+ if (i.ItemId.HasValue)
{
- return true;
+ if (i.ItemId.Value == itemId)
+ {
+ return true;
+ }
+
+ continue;
}
var child = GetLinkedChild(i);
@@ -1506,9 +1584,6 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i.Item2 != null);
}
- [JsonIgnore]
- protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren;
-
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var changesFound = false;
@@ -1529,7 +1604,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)
@@ -1593,8 +1669,8 @@ namespace MediaBrowser.Controller.Entities
/// <param name="user">The user.</param>
/// <param name="datePlayed">The date played.</param>
/// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
- /// <returns>Task.</returns>
- public override void MarkPlayed(User user,
+ public override void MarkPlayed(
+ User user,
DateTime? datePlayed,
bool resetPosition)
{
@@ -1634,7 +1710,6 @@ namespace MediaBrowser.Controller.Entities
/// Marks the unplayed.
/// </summary>
/// <param name="user">The user.</param>
- /// <returns>Task.</returns>
public override void MarkUnplayed(User user)
{
var itemsResult = GetItemList(new InternalItemsQuery
@@ -1671,51 +1746,6 @@ namespace MediaBrowser.Controller.Entities
return !IsPlayed(user);
}
- [JsonIgnore]
- public virtual bool SupportsUserDataFromChildren
- {
- get
- {
- // These are just far too slow.
- if (this is ICollectionFolder)
- {
- return false;
- }
-
- if (this is UserView)
- {
- return false;
- }
-
- if (this is UserRootFolder)
- {
- return false;
- }
-
- if (this is Channel)
- {
- return false;
- }
-
- if (SourceType != SourceType.Library)
- {
- return false;
- }
-
- var iItemByName = this as IItemByName;
- if (iItemByName != null)
- {
- var hasDualAccess = this as IHasDualAccess;
- if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
- {
- return false;
- }
- }
-
- return true;
- }
- }
-
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
{
if (!SupportsUserDataFromChildren)
@@ -1745,20 +1775,15 @@ namespace MediaBrowser.Controller.Entities
{
EnableImages = false
}
- });
+ }).TotalRecordCount;
- double unplayedCount = unplayedQueryResult.TotalRecordCount;
+ dto.UnplayedItemCount = unplayedQueryResult;
- dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount;
-
- if (itemDto != null && itemDto.RecursiveItemCount.HasValue)
+ if (itemDto?.RecursiveItemCount > 0)
{
- if (itemDto.RecursiveItemCount.Value > 0)
- {
- var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100;
- dto.PlayedPercentage = 100 - unplayedPercentage;
- dto.Played = dto.PlayedPercentage.Value >= 100;
- }
+ var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100;
+ dto.PlayedPercentage = 100 - unplayedPercentage;
+ dto.Played = dto.PlayedPercentage.Value >= 100;
}
else
{
@@ -1766,5 +1791,45 @@ namespace MediaBrowser.Controller.Entities
}
}
}
+
+ /// <summary>
+ /// Contains constants used when reporting scan progress.
+ /// </summary>
+ private static class ProgressHelpers
+ {
+ /// <summary>
+ /// Reported after the folders immediate children are retrieved.
+ /// </summary>
+ public const int RetrievedChildren = 5;
+
+ /// <summary>
+ /// Reported after add, updating, or deleting child items from the LibraryManager.
+ /// </summary>
+ public const int UpdatedChildItems = 10;
+
+ /// <summary>
+ /// Reported once subfolders are scanned.
+ /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders].
+ /// </summary>
+ public const int ScannedSubfolders = 50;
+
+ /// <summary>
+ /// Reported once metadata is refreshed.
+ /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed].
+ /// </summary>
+ public const int RefreshedMetadata = 100;
+
+ /// <summary>
+ /// Gets the current progress given the previous step, next step, and progress in between.
+ /// </summary>
+ /// <param name="previousProgressStep">The previous progress step.</param>
+ /// <param name="nextProgressStep">The next progress step.</param>
+ /// <param name="currentProgress">The current progress step.</param>
+ /// <returns>The progress.</returns>
+ public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress)
+ {
+ return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100));
+ }
+ }
}
}
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index db6c85caf7..338f96204d 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -1,10 +1,12 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Extensions;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities
@@ -14,6 +16,23 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Genre : BaseItem, IItemByName
{
+ /// <summary>
+ /// Gets the folder containing the item.
+ /// If the item is a folder, it returns the folder itself.
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [JsonIgnore]
+ public override string ContainingFolderPath => Path;
+
+ [JsonIgnore]
+ public override bool IsDisplayedAsFolder => true;
+
+ [JsonIgnore]
+ public override bool SupportsAncestors => false;
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
@@ -32,20 +51,6 @@ namespace MediaBrowser.Controller.Entities
return 1;
}
- /// <summary>
- /// Returns the folder containing the item.
- /// If the item is a folder, it returns the folder itself.
- /// </summary>
- /// <value>The containing folder path.</value>
- [JsonIgnore]
- public override string ContainingFolderPath => Path;
-
- [JsonIgnore]
- public override bool IsDisplayedAsFolder => true;
-
- [JsonIgnore]
- public override bool SupportsAncestors => false;
-
public override bool IsSaveLocalMetadataEnabled()
{
return true;
@@ -59,14 +64,17 @@ namespace MediaBrowser.Controller.Entities
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
- query.ExcludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+ query.ExcludeItemTypes = new[]
+ {
+ nameof(MusicVideo),
+ nameof(Entities.Audio.Audio),
+ nameof(MusicAlbum),
+ nameof(MusicArtist)
+ };
return LibraryManager.GetItemList(query);
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
public static string GetPath(string name)
{
return GetPath(name, true);
@@ -100,11 +108,13 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// This is called before any metadata refresh and returns true or false indicating if changes were made.
+ /// This is called before any metadata refresh and returns true if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
+ /// <returns>true if the item has change, else false.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
index b84a9fa6f1..89e494ebc3 100644
--- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1819, CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs
index d7d0076681..3aeb7468f2 100644
--- a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs
+++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
namespace MediaBrowser.Controller.Entities
{
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
index 13226b2346..14459624e9 100644
--- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
+++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
namespace MediaBrowser.Controller.Entities
{
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
index a7b60d1688..90d9bdd2d3 100644
--- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs
+++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -18,10 +20,10 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the media sources.
/// </summary>
+ /// <param name="enablePathSubstitution"><c>true</c> to enable path substitution, <c>false</c> to not.</param>
+ /// <returns>A list of media sources.</returns>
List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
List<MediaStream> GetMediaStreams();
-
-
}
}
diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
index f747b5149a..f80f7c304c 100644
--- a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
+++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.LiveTv;
diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
index b027a0cb13..ae01c223ed 100644
--- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs
+++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
@@ -1,7 +1,7 @@
namespace MediaBrowser.Controller.Entities
{
/// <summary>
- /// Interface IHasScreenshots.
+ /// The item has screenshots.
/// </summary>
public interface IHasScreenshots
{
diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs
index 5444f1f523..5f774bbdee 100644
--- a/MediaBrowser.Controller/Entities/IHasSeries.cs
+++ b/MediaBrowser.Controller/Entities/IHasSeries.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,7 +9,7 @@ namespace MediaBrowser.Controller.Entities
public interface IHasSeries
{
/// <summary>
- /// Gets the name of the series.
+ /// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
string SeriesName { get; set; }
diff --git a/MediaBrowser.Controller/Entities/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs
new file mode 100644
index 0000000000..dca5af873f
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasShares.cs
@@ -0,0 +1,11 @@
+#nullable disable
+
+#pragma warning disable CA1819, CS1591
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasShares
+ {
+ Share[] Shares { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
index 6a350212b1..f317a02ff3 100644
--- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
+++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs
index d1f6f2b7e2..f4271678d4 100644
--- a/MediaBrowser.Controller/Entities/IHasTrailers.cs
+++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -37,6 +39,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the trailer count.
/// </summary>
+ /// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
public static int GetTrailerCount(this IHasTrailers item)
=> item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count;
@@ -44,6 +47,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the trailer ids.
/// </summary>
+ /// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item)
{
@@ -68,6 +72,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the trailers.
/// </summary>
+ /// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns>
public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item)
{
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 904752a229..0baa7725e1 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1044, CA1819, CA2227, CS1591
using System;
using System.Collections.Generic;
@@ -12,15 +12,64 @@ namespace MediaBrowser.Controller.Entities
{
public class InternalItemsQuery
{
+ public InternalItemsQuery()
+ {
+ AlbumArtistIds = Array.Empty<Guid>();
+ AlbumIds = Array.Empty<Guid>();
+ AncestorIds = Array.Empty<Guid>();
+ ArtistIds = Array.Empty<Guid>();
+ BlockUnratedItems = Array.Empty<UnratedItem>();
+ BoxSetLibraryFolders = Array.Empty<Guid>();
+ ChannelIds = Array.Empty<Guid>();
+ ContributingArtistIds = Array.Empty<Guid>();
+ DtoOptions = new DtoOptions();
+ EnableTotalRecordCount = true;
+ ExcludeArtistIds = Array.Empty<Guid>();
+ ExcludeInheritedTags = Array.Empty<string>();
+ ExcludeItemIds = Array.Empty<Guid>();
+ ExcludeItemTypes = Array.Empty<string>();
+ ExcludeTags = Array.Empty<string>();
+ GenreIds = Array.Empty<Guid>();
+ Genres = Array.Empty<string>();
+ GroupByPresentationUniqueKey = true;
+ ImageTypes = Array.Empty<ImageType>();
+ IncludeItemTypes = Array.Empty<string>();
+ ItemIds = Array.Empty<Guid>();
+ MediaTypes = Array.Empty<string>();
+ MinSimilarityScore = 20;
+ OfficialRatings = Array.Empty<string>();
+ OrderBy = Array.Empty<ValueTuple<string, SortOrder>>();
+ PersonIds = Array.Empty<Guid>();
+ PersonTypes = Array.Empty<string>();
+ PresetViews = Array.Empty<string>();
+ SeriesStatuses = Array.Empty<SeriesStatus>();
+ SourceTypes = Array.Empty<SourceType>();
+ StudioIds = Array.Empty<Guid>();
+ Tags = Array.Empty<string>();
+ TopParentIds = Array.Empty<Guid>();
+ TrailerTypes = Array.Empty<TrailerType>();
+ VideoTypes = Array.Empty<VideoType>();
+ Years = Array.Empty<int>();
+ }
+
+ public InternalItemsQuery(User? user)
+ : this()
+ {
+ if (user != null)
+ {
+ SetUser(user);
+ }
+ }
+
public bool Recursive { get; set; }
public int? StartIndex { get; set; }
public int? Limit { get; set; }
- public User User { get; set; }
+ public User? User { get; set; }
- public BaseItem SimilarTo { get; set; }
+ public BaseItem? SimilarTo { get; set; }
public bool? IsFolder { get; set; }
@@ -46,7 +95,7 @@ namespace MediaBrowser.Controller.Entities
public string[] ExcludeInheritedTags { get; set; }
- public string[] Genres { get; set; }
+ public IReadOnlyList<string> Genres { get; set; }
public bool? IsSpecialSeason { get; set; }
@@ -56,23 +105,23 @@ namespace MediaBrowser.Controller.Entities
public bool? CollapseBoxSetItems { get; set; }
- public string NameStartsWithOrGreater { get; set; }
+ public string? NameStartsWithOrGreater { get; set; }
- public string NameStartsWith { get; set; }
+ public string? NameStartsWith { get; set; }
- public string NameLessThan { get; set; }
+ public string? NameLessThan { get; set; }
- public string NameContains { get; set; }
+ public string? NameContains { get; set; }
- public string MinSortName { get; set; }
+ public string? MinSortName { get; set; }
- public string PresentationUniqueKey { get; set; }
+ public string? PresentationUniqueKey { get; set; }
- public string Path { get; set; }
+ public string? Path { get; set; }
- public string Name { get; set; }
+ public string? Name { get; set; }
- public string Person { get; set; }
+ public string? Person { get; set; }
public Guid[] PersonIds { get; set; }
@@ -80,7 +129,7 @@ namespace MediaBrowser.Controller.Entities
public Guid[] ExcludeItemIds { get; set; }
- public string AdjacentTo { get; set; }
+ public string? AdjacentTo { get; set; }
public string[] PersonTypes { get; set; }
@@ -116,7 +165,7 @@ namespace MediaBrowser.Controller.Entities
public Guid[] StudioIds { get; set; }
- public Guid[] GenreIds { get; set; }
+ public IReadOnlyList<Guid> GenreIds { get; set; }
public ImageType[] ImageTypes { get; set; }
@@ -162,7 +211,7 @@ namespace MediaBrowser.Controller.Entities
public double? MinCommunityRating { get; set; }
- public Guid[] ChannelIds { get; set; }
+ public IReadOnlyList<Guid> ChannelIds { get; set; }
public int? ParentIndexNumber { get; set; }
@@ -180,29 +229,12 @@ namespace MediaBrowser.Controller.Entities
public Guid ParentId { get; set; }
- public string ParentType { get; set; }
+ public string? ParentType { get; set; }
public Guid[] AncestorIds { get; set; }
public Guid[] TopParentIds { get; set; }
- public BaseItem Parent
- {
- set
- {
- if (value == null)
- {
- ParentId = Guid.Empty;
- ParentType = null;
- }
- else
- {
- ParentId = value.Id;
- ParentType = value.GetType().Name;
- }
- }
- }
-
public string[] PresetViews { get; set; }
public TrailerType[] TrailerTypes { get; set; }
@@ -211,9 +243,9 @@ namespace MediaBrowser.Controller.Entities
public SeriesStatus[] SeriesStatuses { get; set; }
- public string ExternalSeriesId { get; set; }
+ public string? ExternalSeriesId { get; set; }
- public string ExternalId { get; set; }
+ public string? ExternalId { get; set; }
public Guid[] AlbumIds { get; set; }
@@ -221,9 +253,9 @@ namespace MediaBrowser.Controller.Entities
public Guid[] ExcludeArtistIds { get; set; }
- public string AncestorWithPresentationUniqueKey { get; set; }
+ public string? AncestorWithPresentationUniqueKey { get; set; }
- public string SeriesPresentationUniqueKey { get; set; }
+ public string? SeriesPresentationUniqueKey { get; set; }
public bool GroupByPresentationUniqueKey { get; set; }
@@ -233,7 +265,7 @@ namespace MediaBrowser.Controller.Entities
public bool ForceDirect { get; set; }
- public Dictionary<string, string> ExcludeProviderIds { get; set; }
+ public Dictionary<string, string>? ExcludeProviderIds { get; set; }
public bool EnableGroupByMetadataKey { get; set; }
@@ -251,13 +283,13 @@ namespace MediaBrowser.Controller.Entities
public int MinSimilarityScore { get; set; }
- public string HasNoAudioTrackWithLanguage { get; set; }
+ public string? HasNoAudioTrackWithLanguage { get; set; }
- public string HasNoInternalSubtitleTrackWithLanguage { get; set; }
+ public string? HasNoInternalSubtitleTrackWithLanguage { get; set; }
- public string HasNoExternalSubtitleTrackWithLanguage { get; set; }
+ public string? HasNoExternalSubtitleTrackWithLanguage { get; set; }
- public string HasNoSubtitleTrackWithLanguage { get; set; }
+ public string? HasNoSubtitleTrackWithLanguage { get; set; }
public bool? IsDeadArtist { get; set; }
@@ -265,74 +297,29 @@ namespace MediaBrowser.Controller.Entities
public bool? IsDeadPerson { get; set; }
- public InternalItemsQuery()
- {
- AlbumArtistIds = Array.Empty<Guid>();
- AlbumIds = Array.Empty<Guid>();
- AncestorIds = Array.Empty<Guid>();
- ArtistIds = Array.Empty<Guid>();
- BlockUnratedItems = Array.Empty<UnratedItem>();
- BoxSetLibraryFolders = Array.Empty<Guid>();
- ChannelIds = Array.Empty<Guid>();
- ContributingArtistIds = Array.Empty<Guid>();
- DtoOptions = new DtoOptions();
- EnableTotalRecordCount = true;
- ExcludeArtistIds = Array.Empty<Guid>();
- ExcludeInheritedTags = Array.Empty<string>();
- ExcludeItemIds = Array.Empty<Guid>();
- ExcludeItemTypes = Array.Empty<string>();
- ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- ExcludeTags = Array.Empty<string>();
- GenreIds = Array.Empty<Guid>();
- Genres = Array.Empty<string>();
- GroupByPresentationUniqueKey = true;
- HasAnyProviderId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- ImageTypes = Array.Empty<ImageType>();
- IncludeItemTypes = Array.Empty<string>();
- ItemIds = Array.Empty<Guid>();
- MediaTypes = Array.Empty<string>();
- MinSimilarityScore = 20;
- OfficialRatings = Array.Empty<string>();
- OrderBy = Array.Empty<ValueTuple<string, SortOrder>>();
- PersonIds = Array.Empty<Guid>();
- PersonTypes = Array.Empty<string>();
- PresetViews = Array.Empty<string>();
- SeriesStatuses = Array.Empty<SeriesStatus>();
- SourceTypes = Array.Empty<SourceType>();
- StudioIds = Array.Empty<Guid>();
- Tags = Array.Empty<string>();
- TopParentIds = Array.Empty<Guid>();
- TrailerTypes = Array.Empty<TrailerType>();
- VideoTypes = Array.Empty<VideoType>();
- Years = Array.Empty<int>();
- }
-
- public InternalItemsQuery(User user)
- : this()
- {
- SetUser(user);
- }
+ /// <summary>
+ /// Gets or sets a value indicating whether album sub-folders should be returned if they exist.
+ /// </summary>
+ public bool? DisplayAlbumFolders { get; set; }
- public void SetUser(User user)
+ public BaseItem? Parent
{
- if (user != null)
+ set
{
- MaxParentalRating = user.MaxParentalAgeRating;
-
- if (MaxParentalRating.HasValue)
+ if (value == null)
+ {
+ ParentId = Guid.Empty;
+ ParentType = null;
+ }
+ else
{
- BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
- .Where(i => i != UnratedItem.Other.ToString())
- .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+ ParentId = value.Id;
+ ParentType = value.GetType().Name;
}
-
- ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
-
- User = user;
}
}
- public Dictionary<string, string> HasAnyProviderId { get; set; }
+ public Dictionary<string, string>? HasAnyProviderId { get; set; }
public Guid[] AlbumArtistIds { get; set; }
@@ -354,8 +341,25 @@ namespace MediaBrowser.Controller.Entities
public int? MinWidth { get; set; }
- public string SearchTerm { get; set; }
+ public string? SearchTerm { get; set; }
+
+ public string? SeriesTimerId { get; set; }
- public string SeriesTimerId { get; set; }
+ public void SetUser(User user)
+ {
+ MaxParentalRating = user.MaxParentalAgeRating;
+
+ if (MaxParentalRating.HasValue)
+ {
+ string other = UnratedItem.Other.ToString();
+ BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
+ .Where(i => i != other)
+ .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+ }
+
+ ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
+
+ User = user;
+ }
}
}
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
index 4e09ee5736..3e1d892748 100644
--- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -1,11 +1,26 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Entities
{
public class InternalPeopleQuery
{
+ public InternalPeopleQuery()
+ : this(Array.Empty<string>(), Array.Empty<string>())
+ {
+ }
+
+ public InternalPeopleQuery(IReadOnlyList<string> personTypes, IReadOnlyList<string> excludePersonTypes)
+ {
+ PersonTypes = personTypes;
+ ExcludePersonTypes = excludePersonTypes;
+ }
+
/// <summary>
/// Gets or sets the maximum number of items the query should return.
/// </summary>
@@ -13,9 +28,9 @@ namespace MediaBrowser.Controller.Entities
public Guid ItemId { get; set; }
- public string[] PersonTypes { get; set; }
+ public IReadOnlyList<string> PersonTypes { get; }
- public string[] ExcludePersonTypes { get; set; }
+ public IReadOnlyList<string> ExcludePersonTypes { get; }
public int? MaxListOrder { get; set; }
@@ -23,10 +38,8 @@ namespace MediaBrowser.Controller.Entities
public string NameContains { get; set; }
- public InternalPeopleQuery()
- {
- PersonTypes = Array.Empty<string>();
- ExcludePersonTypes = Array.Empty<string>();
- }
+ public User User { get; set; }
+
+ public bool? IsFavorite { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
index 570d8eec0c..ea8555dbfe 100644
--- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs
+++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs
index 8e0f721e77..fd5fef3dc5 100644
--- a/MediaBrowser.Controller/Entities/LinkedChild.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChild.cs
@@ -1,15 +1,20 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.Text.Json.Serialization;
-using MediaBrowser.Model.IO;
namespace MediaBrowser.Controller.Entities
{
public class LinkedChild
{
+ public LinkedChild()
+ {
+ Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ }
+
public string Path { get; set; }
public LinkedChildType Type { get; set; }
@@ -20,7 +25,7 @@ namespace MediaBrowser.Controller.Entities
public string Id { get; set; }
/// <summary>
- /// Serves as a cache.
+ /// Gets or sets the linked item id.
/// </summary>
public Guid? ItemId { get; set; }
@@ -39,41 +44,5 @@ namespace MediaBrowser.Controller.Entities
return child;
}
-
- public LinkedChild()
- {
- Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- }
- }
-
- public enum LinkedChildType
- {
- Manual = 0,
- Shortcut = 1
- }
-
- public class LinkedChildComparer : IEqualityComparer<LinkedChild>
- {
- private readonly IFileSystem _fileSystem;
-
- public LinkedChildComparer(IFileSystem fileSystem)
- {
- _fileSystem = fileSystem;
- }
-
- public bool Equals(LinkedChild x, LinkedChild y)
- {
- if (x.Type == y.Type)
- {
- return _fileSystem.AreEqual(x.Path, y.Path);
- }
-
- return false;
- }
-
- public int GetHashCode(LinkedChild obj)
- {
- return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode();
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
new file mode 100644
index 0000000000..4e58e29429
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
@@ -0,0 +1,35 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class LinkedChildComparer : IEqualityComparer<LinkedChild>
+ {
+ private readonly IFileSystem _fileSystem;
+
+ public LinkedChildComparer(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ public bool Equals(LinkedChild x, LinkedChild y)
+ {
+ if (x.Type == y.Type)
+ {
+ return _fileSystem.AreEqual(x.Path, y.Path);
+ }
+
+ return false;
+ }
+
+ public int GetHashCode(LinkedChild obj)
+ {
+ return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/LinkedChildType.cs b/MediaBrowser.Controller/Entities/LinkedChildType.cs
new file mode 100644
index 0000000000..9ddb7b6202
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/LinkedChildType.cs
@@ -0,0 +1,18 @@
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// The linked child type.
+ /// </summary>
+ public enum LinkedChildType
+ {
+ /// <summary>
+ /// Manually linked child.
+ /// </summary>
+ Manual = 0,
+
+ /// <summary>
+ /// Shortcut linked child.
+ /// </summary>
+ Shortcut = 1
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 8de88cc1b1..e46f99cd57 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1721, CA1819, CS1591
using System;
using System.Collections.Generic;
@@ -47,9 +49,33 @@ namespace MediaBrowser.Controller.Entities.Movies
/// <value>The display order.</value>
public string DisplayOrder { get; set; }
+ [JsonIgnore]
+ private bool IsLegacyBoxSet
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(Path))
+ {
+ return false;
+ }
+
+ if (LinkedChildren.Length > 0)
+ {
+ return false;
+ }
+
+ return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
+ }
+ }
+
+ [JsonIgnore]
+ public override bool IsPreSorted => true;
+
+ public Guid[] LibraryFolderIds { get; set; }
+
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie.ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie);
}
public override double GetDefaultPrimaryImageAspectRatio()
@@ -81,28 +107,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return new List<BaseItem>();
}
- [JsonIgnore]
- private bool IsLegacyBoxSet
- {
- get
- {
- if (string.IsNullOrEmpty(Path))
- {
- return false;
- }
-
- if (LinkedChildren.Length > 0)
- {
- return false;
- }
-
- return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
- }
- }
-
- [JsonIgnore]
- public override bool IsPreSorted => true;
-
public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
{
return true;
@@ -189,8 +193,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return IsVisible(user);
}
- public Guid[] LibraryFolderIds { get; set; }
-
private Guid[] GetLibraryFolderIds(User user)
{
return LibraryManager.GetUserRootFolder().GetChildren(user, true)
@@ -217,8 +219,7 @@ namespace MediaBrowser.Controller.Entities.Movies
private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders)
{
- var boxset = item as BoxSet;
- if (boxset != null)
+ if (item is BoxSet boxset)
{
if (!expandedFolders.Contains(item.Id))
{
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index 8b67aaccc6..b54bbf5eb9 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -142,9 +144,9 @@ namespace MediaBrowser.Controller.Entities.Movies
}
/// <inheritdoc />
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!ProductionYear.HasValue)
{
diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs
index b278a01423..237ad5198c 100644
--- a/MediaBrowser.Controller/Entities/MusicVideo.cs
+++ b/MediaBrowser.Controller/Entities/MusicVideo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,15 +13,15 @@ namespace MediaBrowser.Controller.Entities
{
public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo>
{
- /// <inheritdoc />
- [JsonIgnore]
- public IReadOnlyList<string> Artists { get; set; }
-
public MusicVideo()
{
Artists = Array.Empty<string>();
}
+ /// <inheritdoc />
+ [JsonIgnore]
+ public IReadOnlyList<string> Artists { get; set; }
+
public override UnratedItem GetBlockUnratedType()
{
return UnratedItem.Music;
@@ -34,9 +36,9 @@ namespace MediaBrowser.Controller.Entities
return info;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!ProductionYear.HasValue)
{
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
index 1f3758a73a..687ce1ec8d 100644
--- a/MediaBrowser.Controller/Entities/PeopleHelper.cs
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -100,23 +100,5 @@ namespace MediaBrowser.Controller.Entities
existing.SetProviderId(id.Key, id.Value);
}
}
-
- public static bool ContainsPerson(List<PersonInfo> people, string name)
- {
- if (string.IsNullOrEmpty(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
-
- foreach (var i in people)
- {
- if (string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
index c4fcb0267a..045c1b89fd 100644
--- a/MediaBrowser.Controller/Entities/Person.cs
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -1,9 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Controller.Extensions;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -14,6 +16,26 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo>
{
+ /// <summary>
+ /// Gets the folder containing the item.
+ /// If the item is a folder, it returns the folder itself.
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [JsonIgnore]
+ public override string ContainingFolderPath => Path;
+
+ /// <summary>
+ /// Gets a value indicating whether to enable alpha numeric sorting.
+ /// </summary>
+ [JsonIgnore]
+ public override bool EnableAlphaNumericSorting => false;
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ [JsonIgnore]
+ public override bool SupportsAncestors => false;
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
@@ -47,14 +69,6 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query);
}
- /// <summary>
- /// Returns the folder containing the item.
- /// If the item is a folder, it returns the folder itself.
- /// </summary>
- /// <value>The containing folder path.</value>
- [JsonIgnore]
- public override string ContainingFolderPath => Path;
-
public override bool CanDelete()
{
return false;
@@ -65,15 +79,6 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- [JsonIgnore]
- public override bool EnableAlphaNumericSorting => false;
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
- [JsonIgnore]
- public override bool SupportsAncestors => false;
-
public static string GetPath(string name)
{
return GetPath(name, true);
@@ -124,9 +129,11 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param>
+ /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs
index 4ff9b0955c..2b689ae7e2 100644
--- a/MediaBrowser.Controller/Entities/PersonInfo.cs
+++ b/MediaBrowser.Controller/Entities/PersonInfo.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs
index 1485d4c792..ba6ce189ac 100644
--- a/MediaBrowser.Controller/Entities/Photo.cs
+++ b/MediaBrowser.Controller/Entities/Photo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Text.Json.Serialization;
@@ -16,7 +18,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override Folder LatestItemsIndexContainer => AlbumEntity;
-
[JsonIgnore]
public PhotoAlbum AlbumEntity
{
@@ -25,8 +26,7 @@ namespace MediaBrowser.Controller.Entities
var parents = GetParents();
foreach (var parent in parents)
{
- var photoAlbum = parent as PhotoAlbum;
- if (photoAlbum != null)
+ if (parent is PhotoAlbum photoAlbum)
{
return photoAlbum;
}
@@ -36,6 +36,30 @@ namespace MediaBrowser.Controller.Entities
}
}
+ public string CameraMake { get; set; }
+
+ public string CameraModel { get; set; }
+
+ public string Software { get; set; }
+
+ public double? ExposureTime { get; set; }
+
+ public double? FocalLength { get; set; }
+
+ public ImageOrientation? Orientation { get; set; }
+
+ public double? Aperture { get; set; }
+
+ public double? ShutterSpeed { get; set; }
+
+ public double? Latitude { get; set; }
+
+ public double? Longitude { get; set; }
+
+ public double? Altitude { get; set; }
+
+ public int? IsoSpeedRating { get; set; }
+
public override bool CanDownload()
{
return true;
@@ -69,29 +93,5 @@ namespace MediaBrowser.Controller.Entities
return base.GetDefaultPrimaryImageAspectRatio();
}
-
- public string CameraMake { get; set; }
-
- public string CameraModel { get; set; }
-
- public string Software { get; set; }
-
- public double? ExposureTime { get; set; }
-
- public double? FocalLength { get; set; }
-
- public ImageOrientation? Orientation { get; set; }
-
- public double? Aperture { get; set; }
-
- public double? ShutterSpeed { get; set; }
-
- public double? Latitude { get; set; }
-
- public double? Longitude { get; set; }
-
- public double? Altitude { get; set; }
-
- public int? IsoSpeedRating { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs
index 50f1655f39..64f446eef2 100644
--- a/MediaBrowser.Controller/Entities/Share.cs
+++ b/MediaBrowser.Controller/Entities/Share.cs
@@ -1,12 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Entities
{
- public interface IHasShares
- {
- Share[] Shares { get; set; }
- }
-
public class Share
{
public string UserId { get; set; }
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs
index 9018ddb75e..c8feb1c946 100644
--- a/MediaBrowser.Controller/Entities/Studio.cs
+++ b/MediaBrowser.Controller/Entities/Studio.cs
@@ -1,9 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Controller.Extensions;
+using Diacritics.Extensions;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities
@@ -13,21 +15,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Studio : BaseItem, IItemByName
{
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
- return list;
- }
-
- public override string CreatePresentationUniqueKey()
- {
- return GetUserDataKeys()[0];
- }
-
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
@@ -40,6 +29,22 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override bool SupportsAncestors => false;
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 16;
@@ -65,9 +70,6 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query);
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
public static string GetPath(string name)
{
return GetPath(name, true);
@@ -103,9 +105,11 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param>
+ /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index dc12fbbead..27c3ff81bd 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -32,7 +34,7 @@ namespace MediaBrowser.Controller.Entities.TV
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
/// <summary>
- /// Gets the season in which it aired.
+ /// Gets or sets the season in which it aired.
/// </summary>
/// <value>The aired season.</value>
public int? AirsBeforeSeasonNumber { get; set; }
@@ -42,17 +44,11 @@ namespace MediaBrowser.Controller.Entities.TV
public int? AirsBeforeEpisodeNumber { get; set; }
/// <summary>
- /// This is the ending episode number for double episodes.
+ /// Gets or sets the ending episode number for double episodes.
/// </summary>
/// <value>The index number.</value>
public int? IndexNumberEnd { get; set; }
- public string FindSeriesSortName()
- {
- var series = Series;
- return series == null ? SeriesName : series.SortName;
- }
-
[JsonIgnore]
protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1;
@@ -74,39 +70,8 @@ namespace MediaBrowser.Controller.Entities.TV
[JsonIgnore]
protected override bool EnableDefaultVideoUserDataKeys => false;
- public override double GetDefaultPrimaryImageAspectRatio()
- {
- // hack for tv plugins
- if (SourceType == SourceType.Channel)
- {
- return 0;
- }
-
- return 16.0 / 9;
- }
-
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- var series = Series;
- if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
- {
- var seriesUserDataKeys = series.GetUserDataKeys();
- var take = seriesUserDataKeys.Count;
- if (seriesUserDataKeys.Count > 1)
- {
- take--;
- }
-
- list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000")));
- }
-
- return list;
- }
-
/// <summary>
- /// This Episode's Series Instance.
+ /// Gets the Episode's Series Instance.
/// </summary>
/// <value>The series.</value>
[JsonIgnore]
@@ -151,6 +116,74 @@ namespace MediaBrowser.Controller.Entities.TV
[JsonIgnore]
public string SeasonName { get; set; }
+ [JsonIgnore]
+ public override bool SupportsRemoteImageDownloading
+ {
+ get
+ {
+ if (IsMissingEpisode)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ [JsonIgnore]
+ public bool IsMissingEpisode => LocationType == LocationType.Virtual;
+
+ [JsonIgnore]
+ public Guid SeasonId { get; set; }
+
+ [JsonIgnore]
+ public Guid SeriesId { get; set; }
+
+ public string FindSeriesSortName()
+ {
+ var series = Series;
+ return series == null ? SeriesName : series.SortName;
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ // hack for tv plugins
+ if (SourceType == SourceType.Channel)
+ {
+ return 0;
+ }
+
+ return 16.0 / 9;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ var series = Series;
+ if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
+ {
+ var seriesUserDataKeys = series.GetUserDataKeys();
+ var take = seriesUserDataKeys.Count;
+ if (seriesUserDataKeys.Count > 1)
+ {
+ take--;
+ }
+
+ var newList = seriesUserDataKeys.GetRange(0, take);
+ var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
+ for (int i = 0; i < take; i++)
+ {
+ newList[i] = newList[i] + suffix;
+ }
+
+ newList.AddRange(list);
+ list = newList;
+ }
+
+ return list;
+ }
+
public string FindSeriesPresentationUniqueKey()
{
var series = Series;
@@ -208,8 +241,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>
@@ -232,28 +265,6 @@ namespace MediaBrowser.Controller.Entities.TV
return false;
}
- [JsonIgnore]
- public override bool SupportsRemoteImageDownloading
- {
- get
- {
- if (IsMissingEpisode)
- {
- return false;
- }
-
- return true;
- }
- }
-
- [JsonIgnore]
- public bool IsMissingEpisode => LocationType == LocationType.Virtual;
-
- [JsonIgnore]
- public Guid SeasonId { get; set; }
- [JsonIgnore]
- public Guid SeriesId { get; set; }
-
public Guid FindSeriesId()
{
var series = FindParent<Series>();
@@ -276,7 +287,8 @@ namespace MediaBrowser.Controller.Entities.TV
public override IEnumerable<FileSystemMetadata> GetDeletePaths()
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = Path,
@@ -308,9 +320,9 @@ namespace MediaBrowser.Controller.Entities.TV
return id;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!IsLocked)
{
@@ -318,7 +330,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
try
{
- if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetdata))
+ if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata))
{
hasChanges = true;
}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 93bdd6e706..926c7b0459 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -1,7 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
@@ -35,42 +38,8 @@ namespace MediaBrowser.Controller.Entities.TV
[JsonIgnore]
public override Guid DisplayParentId => SeriesId;
- public override double GetDefaultPrimaryImageAspectRatio()
- {
- double value = 2;
- value /= 3;
-
- return value;
- }
-
- public string FindSeriesSortName()
- {
- var series = Series;
- return series == null ? SeriesName : series.SortName;
- }
-
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- var series = Series;
- if (series != null)
- {
- list.InsertRange(0, series.GetUserDataKeys().Select(i => i + (IndexNumber ?? 0).ToString("000")));
- }
-
- return list;
- }
-
- public override int GetChildCount(User user)
- {
- var result = GetChildren(user, true).Count;
-
- return result;
- }
-
/// <summary>
- /// This Episode's Series Instance.
+ /// Gets this Episode's Series Instance.
/// </summary>
/// <value>The series.</value>
[JsonIgnore]
@@ -104,6 +73,57 @@ namespace MediaBrowser.Controller.Entities.TV
}
}
+ [JsonIgnore]
+ public string SeriesPresentationUniqueKey { get; set; }
+
+ [JsonIgnore]
+ public string SeriesName { get; set; }
+
+ [JsonIgnore]
+ public Guid SeriesId { get; set; }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+
+ public string FindSeriesSortName()
+ {
+ var series = Series;
+ return series == null ? SeriesName : series.SortName;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ var series = Series;
+ if (series != null)
+ {
+ var newList = series.GetUserDataKeys();
+ var suffix = (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture);
+ for (int i = 0; i < newList.Count; i++)
+ {
+ newList[i] = newList[i] + suffix;
+ }
+
+ newList.AddRange(list);
+ list = newList;
+ }
+
+ return list;
+ }
+
+ public override int GetChildCount(User user)
+ {
+ var result = GetChildren(user, true).Count;
+
+ return result;
+ }
+
public override string CreatePresentationUniqueKey()
{
if (IndexNumber.HasValue)
@@ -111,7 +131,7 @@ namespace MediaBrowser.Controller.Entities.TV
var series = Series;
if (series != null)
{
- return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000");
+ return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture);
}
}
@@ -124,7 +144,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)
@@ -146,6 +166,9 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Gets the episodes.
/// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="options">The options to use.</param>
+ /// <returns>Set of episodes.</returns>
public List<BaseItem> GetEpisodes(User user, DtoOptions options)
{
return GetEpisodes(Series, user, options);
@@ -182,15 +205,6 @@ namespace MediaBrowser.Controller.Entities.TV
return UnratedItem.Series;
}
- [JsonIgnore]
- public string SeriesPresentationUniqueKey { get; set; }
-
- [JsonIgnore]
- public string SeriesName { get; set; }
-
- [JsonIgnore]
- public Guid SeriesId { get; set; }
-
public string FindSeriesPresentationUniqueKey()
{
var series = Series;
@@ -230,10 +244,11 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
+ /// <param name="replaceAllMetadata"><c>true</c> to replace metdata, <c>false</c> to not.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
{
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 72c696c1ae..beda504b91 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -57,8 +59,11 @@ namespace MediaBrowser.Controller.Entities.TV
public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
/// <summary>
- /// airdate, dvd or absolute.
+ /// Gets or sets the display order.
/// </summary>
+ /// <remarks>
+ /// Valid options are airdate, dvd or absolute.
+ /// </remarks>
public string DisplayOrder { get; set; }
/// <summary>
@@ -67,6 +72,9 @@ namespace MediaBrowser.Controller.Entities.TV
/// <value>The status.</value>
public SeriesStatus? Status { get; set; }
+ [JsonIgnore]
+ public override bool StopRefreshIfLocalMetadataFound => false;
+
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
@@ -107,7 +115,7 @@ namespace MediaBrowser.Controller.Entities.TV
return key;
}
- return key + "-" + string.Join("-", folders);
+ return key + "-" + string.Join('-', folders);
}
private static string GetUniqueSeriesKey(BaseItem series)
@@ -151,7 +159,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
}
query.IsVirtualItem = false;
@@ -169,14 +177,12 @@ namespace MediaBrowser.Controller.Entities.TV
{
var list = base.GetUserDataKeys();
- var key = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(key))
+ if (this.TryGetProviderId(MetadataProvider.Imdb, out var key))
{
list.Insert(0, key);
}
- key = this.GetProviderId(MetadataProvider.Tvdb);
- if (!string.IsNullOrEmpty(key))
+ if (this.TryGetProviderId(MetadataProvider.Tvdb, out key))
{
list.Insert(0, key);
}
@@ -207,8 +213,8 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
- query.IncludeItemTypes = new[] { typeof(Season).Name };
- query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
+ query.IncludeItemTypes = new[] { nameof(Season) };
+ query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
if (user != null && !user.DisplayMissingEpisodes)
{
@@ -228,12 +234,12 @@ namespace MediaBrowser.Controller.Entities.TV
query.SeriesPresentationUniqueKey = seriesKey;
if (query.OrderBy.Count == 0)
{
- query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
+ query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
}
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) };
}
query.IsVirtualItem = false;
@@ -253,8 +259,8 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name },
- OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ IncludeItemTypes = new[] { nameof(Episode), nameof(Season) },
+ OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
@@ -318,20 +324,13 @@ namespace MediaBrowser.Controller.Entities.TV
cancellationToken.ThrowIfCancellationRequested();
- var skipItem = false;
-
- var episode = item as Episode;
-
- if (episode != null
+ bool skipItem = item is Episode episode
&& refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh
&& !refreshOptions.ReplaceAllMetadata
&& episode.IsMissingEpisode
&& episode.LocationType == LocationType.Virtual
&& episode.PremiereDate.HasValue
- && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30)
- {
- skipItem = true;
- }
+ && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30;
if (!skipItem)
{
@@ -364,8 +363,8 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
- IncludeItemTypes = new[] { typeof(Episode).Name },
- OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ IncludeItemTypes = new[] { nameof(Episode) },
+ OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
if (user != null)
@@ -398,6 +397,10 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Filters the episodes by season.
/// </summary>
+ /// <param name="episodes">The episodes.</param>
+ /// <param name="parentSeason">The season.</param>
+ /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
+ /// <returns>The set of episodes.</returns>
public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials)
{
var seasonNumber = parentSeason.IndexNumber;
@@ -428,6 +431,10 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Filters the episodes by season.
/// </summary>
+ /// <param name="episodes">The episodes.</param>
+ /// <param name="seasonNumber">The season.</param>
+ /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
+ /// <returns>The set of episodes.</returns>
public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
{
if (!includeSpecials || seasonNumber < 1)
@@ -450,10 +457,9 @@ namespace MediaBrowser.Controller.Entities.TV
});
}
-
protected override bool GetBlockUnratedValue(User user)
{
- return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series.ToString());
+ return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series);
}
public override UnratedItem GetBlockUnratedType()
@@ -504,8 +510,5 @@ namespace MediaBrowser.Controller.Entities.TV
return list;
}
-
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 9ae8ad7087..1c558d4196 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1819, CS1591
using System;
using System.Collections.Generic;
@@ -21,6 +23,9 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
}
+ [JsonIgnore]
+ public override bool StopRefreshIfLocalMetadataFound => false;
+
public TrailerType[] TrailerTypes { get; set; }
public override double GetDefaultPrimaryImageAspectRatio()
@@ -43,9 +48,9 @@ namespace MediaBrowser.Controller.Entities
return info;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!ProductionYear.HasValue)
{
@@ -95,8 +100,5 @@ namespace MediaBrowser.Controller.Entities
return list;
}
-
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs
index db63c42e4c..50ba9ef308 100644
--- a/MediaBrowser.Controller/Entities/UserItemData.cs
+++ b/MediaBrowser.Controller/Entities/UserItemData.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,6 +12,13 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class UserItemData
{
+ public const double MinLikeValue = 6.5;
+
+ /// <summary>
+ /// The _rating.
+ /// </summary>
+ private double? _rating;
+
/// <summary>
/// Gets or sets the user id.
/// </summary>
@@ -23,11 +32,6 @@ namespace MediaBrowser.Controller.Entities
public string Key { get; set; }
/// <summary>
- /// The _rating.
- /// </summary>
- private double? _rating;
-
- /// <summary>
/// Gets or sets the users 0-10 rating.
/// </summary>
/// <value>The rating.</value>
@@ -91,10 +95,8 @@ namespace MediaBrowser.Controller.Entities
/// <value>The index of the subtitle stream.</value>
public int? SubtitleStreamIndex { get; set; }
- public const double MinLikeValue = 6.5;
-
/// <summary>
- /// This is an interpreted property to indicate likes or dislikes
+ /// Gets or sets a value indicating whether the item is liked or not.
/// This should never be serialized.
/// </summary>
/// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index 7f7224ae07..f3bf4749d2 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -19,22 +21,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class UserRootFolder : Folder
{
- private List<Guid> _childrenIds = null;
private readonly object _childIdsLock = new object();
- protected override List<BaseItem> LoadChildren()
- {
- lock (_childIdsLock)
- {
- if (_childrenIds == null)
- {
- var list = base.LoadChildren();
- _childrenIds = list.Select(i => i.Id).ToList();
- return list;
- }
-
- return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
- }
- }
+ private List<Guid> _childrenIds = null;
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
@@ -42,6 +30,12 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override bool SupportsPlayedStatus => false;
+ [JsonIgnore]
+ protected override bool SupportsShortcutChildren => true;
+
+ [JsonIgnore]
+ public override bool IsPreSorted => true;
+
private void ClearCache()
{
lock (_childIdsLock)
@@ -50,6 +44,21 @@ namespace MediaBrowser.Controller.Entities
}
}
+ protected override List<BaseItem> LoadChildren()
+ {
+ lock (_childIdsLock)
+ {
+ if (_childrenIds == null)
+ {
+ var list = base.LoadChildren();
+ _childrenIds = list.Select(i => i.Id).ToList();
+ return list;
+ }
+
+ return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
+ }
+ }
+
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (query.Recursive)
@@ -71,12 +80,6 @@ namespace MediaBrowser.Controller.Entities
return GetChildren(user, true).Count;
}
- [JsonIgnore]
- protected override bool SupportsShortcutChildren => true;
-
- [JsonIgnore]
- public override bool IsPreSorted => true;
-
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList();
@@ -85,10 +88,10 @@ namespace MediaBrowser.Controller.Entities
return list;
}
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
ClearCache();
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase))
{
@@ -106,11 +109,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index b1da4d64cb..62f3c4b557 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -13,22 +15,57 @@ namespace MediaBrowser.Controller.Entities
{
public class UserView : Folder, IHasCollectionType
{
- /// <inheritdoc />
+ private static readonly string[] _viewTypesEligibleForGrouping = new string[]
+ {
+ Model.Entities.CollectionType.Movies,
+ Model.Entities.CollectionType.TvShows,
+ string.Empty
+ };
+
+ private static readonly string[] _originalFolderViewTypes = new string[]
+ {
+ Model.Entities.CollectionType.Books,
+ Model.Entities.CollectionType.MusicVideos,
+ Model.Entities.CollectionType.HomeVideos,
+ Model.Entities.CollectionType.Photos,
+ Model.Entities.CollectionType.Music,
+ Model.Entities.CollectionType.BoxSets
+ };
+
+ public static ITVSeriesManager TVSeriesManager { get; set; }
+
+ /// <summary>
+ /// Gets or sets the view type.
+ /// </summary>
public string ViewType { get; set; }
- /// <inheritdoc />
+ /// <summary>
+ /// Gets or sets the display parent id.
+ /// </summary>
public new Guid DisplayParentId { get; set; }
- /// <inheritdoc />
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
public Guid? UserId { get; set; }
- public static ITVSeriesManager TVSeriesManager;
-
/// <inheritdoc />
[JsonIgnore]
public string CollectionType => ViewType;
/// <inheritdoc />
+ [JsonIgnore]
+ public override bool SupportsInheritedParentImages => false;
+
+ /// <inheritdoc />
+ [JsonIgnore]
+ public override bool SupportsPlayedStatus => false;
+
+ /// <inheritdoc />
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ /// <inheritdoc />
public override IEnumerable<Guid> GetIdsForAncestorQuery()
{
if (!DisplayParentId.Equals(Guid.Empty))
@@ -45,17 +82,13 @@ namespace MediaBrowser.Controller.Entities
}
}
- [JsonIgnore]
- public override bool SupportsInheritedParentImages => false;
-
- [JsonIgnore]
- public override bool SupportsPlayedStatus => false;
-
+ /// <inheritdoc />
public override int GetChildCount(User user)
{
return GetChildren(user, true).Count;
}
+ /// <inheritdoc />
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
var parent = this as Folder;
@@ -73,12 +106,10 @@ namespace MediaBrowser.Controller.Entities
.GetUserItems(parent, this, CollectionType, query);
}
+ /// <inheritdoc />
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
- if (query == null)
- {
- query = new InternalItemsQuery(user);
- }
+ query ??= new InternalItemsQuery(user);
query.EnableTotalRecordCount = false;
var result = GetItemList(query);
@@ -86,16 +117,19 @@ namespace MediaBrowser.Controller.Entities
return result.ToList();
}
+ /// <inheritdoc />
public override bool CanDelete()
{
return false;
}
+ /// <inheritdoc />
public override bool IsSaveLocalMetadataEnabled()
{
return true;
}
+ /// <inheritdoc />
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
query.SetUser(user);
@@ -106,32 +140,26 @@ namespace MediaBrowser.Controller.Entities
return GetItemList(query);
}
+ /// <inheritdoc />
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return GetChildren(user, false);
}
- private static string[] UserSpecificViewTypes = new string[]
- {
- Model.Entities.CollectionType.Playlists
- };
-
public static bool IsUserSpecific(Folder folder)
{
- var collectionFolder = folder as ICollectionFolder;
-
- if (collectionFolder == null)
+ if (folder is not ICollectionFolder collectionFolder)
{
return false;
}
- var supportsUserSpecific = folder as ISupportsUserSpecificView;
- if (supportsUserSpecific != null && supportsUserSpecific.EnableUserSpecificView)
+ if (folder is ISupportsUserSpecificView supportsUserSpecific
+ && supportsUserSpecific.EnableUserSpecificView)
{
return true;
}
- return UserSpecificViewTypes.Contains(collectionFolder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return string.Equals(Model.Entities.CollectionType.Playlists, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase);
}
public static bool IsEligibleForGrouping(Folder folder)
@@ -140,39 +168,19 @@ namespace MediaBrowser.Controller.Entities
&& IsEligibleForGrouping(collectionFolder.CollectionType);
}
- private static string[] ViewTypesEligibleForGrouping = new string[]
- {
- Model.Entities.CollectionType.Movies,
- Model.Entities.CollectionType.TvShows,
- string.Empty
- };
-
public static bool IsEligibleForGrouping(string viewType)
{
- return ViewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
}
- private static string[] OriginalFolderViewTypes = new string[]
- {
- Model.Entities.CollectionType.Books,
- Model.Entities.CollectionType.MusicVideos,
- Model.Entities.CollectionType.HomeVideos,
- Model.Entities.CollectionType.Photos,
- Model.Entities.CollectionType.Music,
- Model.Entities.CollectionType.BoxSets
- };
-
public static bool EnableOriginalFolder(string viewType)
{
- return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ 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;
}
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index b384b27d1a..266fda767d 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -53,17 +55,17 @@ 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)
{
case CollectionType.Folders:
- return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), queryParent, query);
+ return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query);
case CollectionType.TvShows:
return GetTvView(queryParent, user, query);
@@ -108,7 +110,7 @@ namespace MediaBrowser.Controller.Entities
return GetMovieMovies(queryParent, user, query);
case SpecialFolder.MovieCollections:
- return GetMovieCollections(queryParent, user, query);
+ return GetMovieCollections(user, query);
case SpecialFolder.TvFavoriteEpisodes:
return GetFavoriteEpisodes(queryParent, user, query);
@@ -120,7 +122,7 @@ namespace MediaBrowser.Controller.Entities
{
if (queryParent is UserView)
{
- return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), queryParent, query);
+ return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), query);
}
return queryParent.GetItems(query);
@@ -142,7 +144,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
}
return parent.QueryRecursive(query);
@@ -158,7 +160,7 @@ namespace MediaBrowser.Controller.Entities
GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)
};
- return GetResult(list, parent, query);
+ return GetResult(list, query);
}
private QueryResult<BaseItem> GetFavoriteMovies(Folder parent, User user, InternalItemsQuery query)
@@ -167,7 +169,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
@@ -178,7 +180,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
@@ -189,7 +191,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
return _libraryManager.GetItemsResult(query);
}
@@ -200,15 +202,15 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
- private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query)
+ private QueryResult<BaseItem> GetMovieCollections(User user, InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+ query.IncludeItemTypes = new[] { nameof(BoxSet) };
query.SetUser(user);
query.Recursive = true;
@@ -217,26 +219,25 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
-
+ query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
+ query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
query.IsResumable = true;
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -255,10 +256,9 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Movie).Name },
+ IncludeItemTypes = new[] { nameof(Movie) },
Recursive = true,
EnableTotalRecordCount = false
-
}).Items
.SelectMany(i => i.Genres)
.DistinctNames()
@@ -275,9 +275,9 @@ namespace MediaBrowser.Controller.Entities
}
})
.Where(i => i != null)
- .Select(i => GetUserViewWithName(i.Name, SpecialFolder.MovieGenre, i.SortName, parent));
+ .Select(i => GetUserViewWithName(SpecialFolder.MovieGenre, i.SortName, parent));
- return GetResult(genres, parent, query);
+ return GetResult(genres, query);
}
private QueryResult<BaseItem> GetMovieGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
@@ -287,7 +287,7 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { displayParent.Id };
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ query.IncludeItemTypes = new[] { nameof(Movie) };
return _libraryManager.GetItemsResult(query);
}
@@ -323,18 +323,17 @@ namespace MediaBrowser.Controller.Entities
GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)
};
- return GetResult(list, parent, query);
+ return GetResult(list, query);
}
private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
-
+ query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
query.IsVirtualItem = false;
return ConvertToResult(_libraryManager.GetItemList(query));
@@ -344,25 +343,28 @@ 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);
+ var result = _tvSeriesManager.GetNextUp(
+ new NextUpQuery
+ {
+ Limit = query.Limit,
+ StartIndex = query.StartIndex,
+ UserId = query.User.Id
+ },
+ parentFolders,
+ query.DtoOptions);
return result;
}
private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
+ query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
query.IsResumable = true;
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IncludeItemTypes = new[] { nameof(Episode) };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -373,7 +375,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
@@ -382,7 +384,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { typeof(Series).Name },
+ IncludeItemTypes = new[] { nameof(Series) },
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -401,9 +403,9 @@ namespace MediaBrowser.Controller.Entities
}
})
.Where(i => i != null)
- .Select(i => GetUserViewWithName(i.Name, SpecialFolder.TvGenre, i.SortName, parent));
+ .Select(i => GetUserViewWithName(SpecialFolder.TvGenre, i.SortName, parent));
- return GetResult(genres, parent, query);
+ return GetResult(genres, query);
}
private QueryResult<BaseItem> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
@@ -413,7 +415,7 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { displayParent.Id };
query.SetUser(user);
- query.IncludeItemTypes = new[] { typeof(Series).Name };
+ query.IncludeItemTypes = new[] { nameof(Series) };
return _libraryManager.GetItemsResult(query);
}
@@ -430,13 +432,12 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetResult<T>(
IEnumerable<T> items,
- BaseItem queryParent,
InternalItemsQuery query)
where T : BaseItem
{
items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager));
- return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config);
+ return PostFilterAndSort(items, null, query, _libraryManager);
}
public static bool FilterItem(BaseItem item, InternalItemsQuery query)
@@ -444,12 +445,11 @@ namespace MediaBrowser.Controller.Entities
return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager);
}
- public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items,
- BaseItem queryParent,
+ public static QueryResult<BaseItem> PostFilterAndSort(
+ IEnumerable<BaseItem> items,
int? totalRecordLimit,
InternalItemsQuery query,
- ILibraryManager libraryManager,
- IServerConfigurationManager configurationManager)
+ ILibraryManager libraryManager)
{
var user = query.User;
@@ -790,7 +790,7 @@ namespace MediaBrowser.Controller.Entities
}
// Apply genre filter
- if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
+ if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
{
return false;
}
@@ -821,7 +821,7 @@ namespace MediaBrowser.Controller.Entities
}
// Apply genre filter
- if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id =>
+ if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id =>
{
var genreItem = libraryManager.GetItemById(id);
return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase);
@@ -998,7 +998,7 @@ namespace MediaBrowser.Controller.Entities
return new BaseItem[] { parent };
}
- private UserView GetUserViewWithName(string name, string type, string sortName, BaseItem parent)
+ private UserView GetUserViewWithName(string type, string sortName, BaseItem parent)
{
return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N", CultureInfo.InvariantCulture), type, sortName);
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 07f3818811..7dd95b85cf 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -26,6 +28,14 @@ namespace MediaBrowser.Controller.Entities
ISupportsPlaceHolders,
IHasMediaSources
{
+ public Video()
+ {
+ AdditionalParts = Array.Empty<string>();
+ LocalAlternateVersions = Array.Empty<string>();
+ SubtitleFiles = Array.Empty<string>();
+ LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ }
+
[JsonIgnore]
public string PrimaryVersionId { get; set; }
@@ -72,30 +82,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- public void SetPrimaryVersionId(string id)
- {
- if (string.IsNullOrEmpty(id))
- {
- PrimaryVersionId = null;
- }
- else
- {
- PrimaryVersionId = id;
- }
-
- PresentationUniqueKey = CreatePresentationUniqueKey();
- }
-
- public override string CreatePresentationUniqueKey()
- {
- if (!string.IsNullOrEmpty(PrimaryVersionId))
- {
- return PrimaryVersionId;
- }
-
- return base.CreatePresentationUniqueKey();
- }
-
[JsonIgnore]
public override bool SupportsThemeMedia => true;
@@ -143,50 +129,12 @@ namespace MediaBrowser.Controller.Entities
/// <value>The video3 D format.</value>
public Video3DFormat? Video3DFormat { get; set; }
- public string[] GetPlayableStreamFileNames()
- {
- var videoType = VideoType;
-
- if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.BluRay)
- {
- videoType = VideoType.BluRay;
- }
- else if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.Dvd)
- {
- videoType = VideoType.Dvd;
- }
- else
- {
- return Array.Empty<string>();
- }
-
- throw new NotImplementedException();
- }
-
/// <summary>
/// Gets or sets the aspect ratio.
/// </summary>
/// <value>The aspect ratio.</value>
public string AspectRatio { get; set; }
- public Video()
- {
- AdditionalParts = Array.Empty<string>();
- LocalAlternateVersions = Array.Empty<string>();
- SubtitleFiles = Array.Empty<string>();
- LinkedAlternateVersions = Array.Empty<LinkedChild>();
- }
-
- public override bool CanDownload()
- {
- if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay)
- {
- return false;
- }
-
- return IsFileProtocol;
- }
-
[JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
@@ -214,16 +162,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0;
- public IEnumerable<Guid> GetAdditionalPartIds()
- {
- return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
- }
-
- public IEnumerable<Guid> GetLocalAlternateVersionIds()
- {
- return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
- }
-
public static ILiveTvManager LiveTvManager { get; set; }
[JsonIgnore]
@@ -240,37 +178,77 @@ namespace MediaBrowser.Controller.Entities
}
}
- protected override bool IsActiveRecording()
+ [JsonIgnore]
+ public bool IsCompleteMedia
{
- return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+ get
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase);
+ }
+
+ return !IsActiveRecording();
+ }
}
- public override bool CanDelete()
+ [JsonIgnore]
+ protected virtual bool EnableDefaultVideoUserDataKeys => true;
+
+ [JsonIgnore]
+ public override string ContainingFolderPath
{
- if (IsActiveRecording())
+ get
{
- return false;
- }
+ if (IsStacked)
+ {
+ return System.IO.Path.GetDirectoryName(Path);
+ }
- return base.CanDelete();
+ if (!IsPlaceHolder)
+ {
+ if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
+ {
+ return Path;
+ }
+ }
+
+ return base.ContainingFolderPath;
+ }
}
[JsonIgnore]
- public bool IsCompleteMedia
+ public override string FileNameWithoutExtension
{
get
{
- if (SourceType == SourceType.Channel)
+ if (IsFileProtocol)
{
- return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase);
+ if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
+ {
+ return System.IO.Path.GetFileName(Path);
+ }
+
+ return System.IO.Path.GetFileNameWithoutExtension(Path);
}
- return !IsActiveRecording();
+ return null;
}
}
+ /// <summary>
+ /// Gets a value indicating whether [is3 D].
+ /// </summary>
+ /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
[JsonIgnore]
- protected virtual bool EnableDefaultVideoUserDataKeys => true;
+ public bool Is3D => Video3DFormat.HasValue;
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ [JsonIgnore]
+ public override string MediaType => Model.Entities.MediaType.Video;
public override List<string> GetUserDataKeys()
{
@@ -311,6 +289,65 @@ namespace MediaBrowser.Controller.Entities
return list;
}
+ public void SetPrimaryVersionId(string id)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ PrimaryVersionId = null;
+ }
+ else
+ {
+ PrimaryVersionId = id;
+ }
+
+ PresentationUniqueKey = CreatePresentationUniqueKey();
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ if (!string.IsNullOrEmpty(PrimaryVersionId))
+ {
+ return PrimaryVersionId;
+ }
+
+ return base.CreatePresentationUniqueKey();
+ }
+
+ public override bool CanDownload()
+ {
+ if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay)
+ {
+ return false;
+ }
+
+ return IsFileProtocol;
+ }
+
+ protected override bool IsActiveRecording()
+ {
+ return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+ }
+
+ public override bool CanDelete()
+ {
+ if (IsActiveRecording())
+ {
+ return false;
+ }
+
+ return base.CanDelete();
+ }
+
+ public IEnumerable<Guid> GetAdditionalPartIds()
+ {
+ return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
+ }
+
+ public IEnumerable<Guid> GetLocalAlternateVersionIds()
+ {
+ return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
+ }
+
private string GetUserDataKey(string providerId)
{
var key = providerId + "-" + ExtraType.ToString().ToLowerInvariant();
@@ -346,47 +383,6 @@ namespace MediaBrowser.Controller.Entities
.OrderBy(i => i.SortName);
}
- [JsonIgnore]
- public override string ContainingFolderPath
- {
- get
- {
- if (IsStacked)
- {
- return System.IO.Path.GetDirectoryName(Path);
- }
-
- if (!IsPlaceHolder)
- {
- if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
- {
- return Path;
- }
- }
-
- return base.ContainingFolderPath;
- }
- }
-
- [JsonIgnore]
- public override string FileNameWithoutExtension
- {
- get
- {
- if (IsFileProtocol)
- {
- if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
- {
- return System.IO.Path.GetFileName(Path);
- }
-
- return System.IO.Path.GetFileNameWithoutExtension(Path);
- }
-
- return null;
- }
- }
-
internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
{
var updateType = base.UpdateFromResolvedItem(newItem);
@@ -415,45 +411,6 @@ namespace MediaBrowser.Controller.Entities
return updateType;
}
- public static string[] QueryPlayableStreamFiles(string rootPath, VideoType videoType)
- {
- if (videoType == VideoType.Dvd)
- {
- return FileSystem.GetFiles(rootPath, new[] { ".vob" }, false, true)
- .OrderByDescending(i => i.Length)
- .ThenBy(i => i.FullName)
- .Take(1)
- .Select(i => i.FullName)
- .ToArray();
- }
-
- if (videoType == VideoType.BluRay)
- {
- return FileSystem.GetFiles(rootPath, new[] { ".m2ts" }, false, true)
- .OrderByDescending(i => i.Length)
- .ThenBy(i => i.FullName)
- .Take(1)
- .Select(i => i.FullName)
- .ToArray();
- }
-
- return Array.Empty<string>();
- }
-
- /// <summary>
- /// Gets a value indicating whether [is3 D].
- /// </summary>
- /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public bool Is3D => Video3DFormat.HasValue;
-
- /// <summary>
- /// Gets the type of the media.
- /// </summary>
- /// <value>The type of the media.</value>
- [JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Video;
-
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
@@ -525,7 +482,8 @@ namespace MediaBrowser.Controller.Entities
{
if (!IsInMixedFolder)
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = ContainingFolderPath,
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
index b2e4d307a6..0853200dd1 100644
--- a/MediaBrowser.Controller/Entities/Year.cs
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -13,22 +15,33 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Year : BaseItem, IItemByName
{
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
+ [JsonIgnore]
+ public override bool SupportsAncestors => false;
- list.Insert(0, "Year-" + Name);
- return list;
- }
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
[JsonIgnore]
public override string ContainingFolderPath => Path;
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, "Year-" + Name);
+ return list;
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
@@ -37,14 +50,6 @@ namespace MediaBrowser.Controller.Entities
return value;
}
- [JsonIgnore]
- public override bool SupportsAncestors => false;
-
- public override bool CanDelete()
- {
- return false;
- }
-
public override bool IsSaveLocalMetadataEnabled()
{
return true;
@@ -74,9 +79,6 @@ namespace MediaBrowser.Controller.Entities
return null;
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
public static string GetPath(string name)
{
return GetPath(name, true);
@@ -110,11 +112,13 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// This is called before any metadata refresh and returns true or false indicating if changes were made.
+ /// This is called before any metadata refresh and returns true if changes were made.
/// </summary>
- public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
+ /// <returns>true if the item has change, else false.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
- var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
var newPath = GetRebasedPath();
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
diff --git a/MediaBrowser.Controller/Events/IEventConsumer.cs b/MediaBrowser.Controller/Events/IEventConsumer.cs
index 5c4ab5d8dd..93005134a7 100644
--- a/MediaBrowser.Controller/Events/IEventConsumer.cs
+++ b/MediaBrowser.Controller/Events/IEventConsumer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Events
diff --git a/MediaBrowser.Controller/Events/IEventManager.cs b/MediaBrowser.Controller/Events/IEventManager.cs
index a1f40b3a6d..074e3f1fe4 100644
--- a/MediaBrowser.Controller/Events/IEventManager.cs
+++ b/MediaBrowser.Controller/Events/IEventManager.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Events
diff --git a/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs
index 46d7e5a17a..3a331ad00f 100644
--- a/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Session;
namespace MediaBrowser.Controller.Events.Session
diff --git a/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs
index aab19cc46a..deeaaf55de 100644
--- a/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Session;
namespace MediaBrowser.Controller.Events.Session
diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs
index b06046c05a..0dd8b0dbfd 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs
index dfadc9f61f..c1d503a7eb 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs
index 045a600272..7a9866834a 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
index 7510b62b88..0f27be9bb7 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs
@@ -1,18 +1,18 @@
-using Jellyfin.Data.Events;
-using MediaBrowser.Common.Plugins;
+using Jellyfin.Data.Events;
+using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Controller.Events.Updates
{
/// <summary>
/// An event that occurs when a plugin is uninstalled.
/// </summary>
- public class PluginUninstalledEventArgs : GenericEventArgs<IPlugin>
+ public class PluginUninstalledEventArgs : GenericEventArgs<PluginInfo>
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginUninstalledEventArgs"/> class.
/// </summary>
/// <param name="arg">The plugin.</param>
- public PluginUninstalledEventArgs(IPlugin arg) : base(arg)
+ public PluginUninstalledEventArgs(PluginInfo arg) : base(arg)
{
}
}
diff --git a/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs
index 661ca066a8..b078e06dc8 100644
--- a/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs
+++ b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs
@@ -1,4 +1,4 @@
-using Jellyfin.Data.Events;
+using Jellyfin.Data.Events;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Controller.Events.Updates
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 4c2209b67c..f9285c7682 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -9,6 +9,12 @@ namespace MediaBrowser.Controller.Extensions
public static class ConfigurationExtensions
{
/// <summary>
+ /// The key for a setting that specifies the default redirect path
+ /// to use for requests where the URL base prefix is invalid or missing..
+ /// </summary>
+ public const string DefaultRedirectKey = "DefaultRedirectPath";
+
+ /// <summary>
/// The key for a setting that indicates whether the application should host web client content.
/// </summary>
public const string HostWebClientKey = "hostwebclient";
diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs
deleted file mode 100644
index 3cc1f328a9..0000000000
--- a/MediaBrowser.Controller/Extensions/StringExtensions.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace MediaBrowser.Controller.Extensions
-{
- /// <summary>
- /// Class BaseExtensions.
- /// </summary>
- public static class StringExtensions
- {
- public static string RemoveDiacritics(this string text)
- {
- if (text == null)
- {
- throw new ArgumentNullException(nameof(text));
- }
-
- var chars = Normalize(text, NormalizationForm.FormD)
- .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark);
-
- return Normalize(string.Concat(chars), NormalizationForm.FormC);
- }
-
- private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true)
- {
- if (stripStringOnFailure)
- {
- try
- {
- return text.Normalize(form);
- }
- catch (ArgumentException)
- {
- // will throw if input contains invalid unicode chars
- // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
- text = Regex.Replace(text, "([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])", "");
- return Normalize(text, form, false);
- }
- }
-
- try
- {
- return text.Normalize(form);
- }
- catch (ArgumentException)
- {
- // if it still fails, return the original text
- return text;
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
index 856b91b5d0..1678d50675 100644
--- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs
+++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using Jellyfin.Data.Entities;
@@ -12,14 +14,21 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the display preferences for the user and client.
/// </summary>
+ /// <remarks>
+ /// This will create the display preferences if it does not exist, but it will not save automatically.
+ /// </remarks>
/// <param name="userId">The user's id.</param>
+ /// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
/// <returns>The associated display preferences.</returns>
- DisplayPreferences GetDisplayPreferences(Guid userId, string client);
+ DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client);
/// <summary>
/// Gets the default item display preferences for the user and client.
/// </summary>
+ /// <remarks>
+ /// This will create the item display preferences if it does not exist, but it will not save automatically.
+ /// </remarks>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
@@ -35,15 +44,26 @@ namespace MediaBrowser.Controller
IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client);
/// <summary>
- /// Saves changes to the provided display preferences.
+ /// Gets all of the custom item display preferences for the user and client.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="client">The client string.</param>
+ /// <returns>The dictionary of custom item display preferences.</returns>
+ Dictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client);
+
+ /// <summary>
+ /// Sets the custom item display preference for the user and client.
/// </summary>
- /// <param name="preferences">The display preferences to save.</param>
- void SaveChanges(DisplayPreferences preferences);
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="client">The client id.</param>
+ /// <param name="customPreferences">A dictionary of custom item display preferences.</param>
+ void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string> customPreferences);
/// <summary>
- /// Saves changes to the provided item display preferences.
+ /// Saves changes made to the database.
/// </summary>
- /// <param name="preferences">The item display preferences to save.</param>
- void SaveChanges(ItemDisplayPreferences preferences);
+ void SaveChanges();
}
}
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
index 9bc4cac39d..b8a0bf3315 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,
@@ -111,5 +111,4 @@ namespace MediaBrowser.Controller.IO
return returnResult;
}
}
-
}
diff --git a/MediaBrowser.Controller/IResourceFileManager.cs b/MediaBrowser.Controller/IResourceFileManager.cs
deleted file mode 100644
index 26f0424b7a..0000000000
--- a/MediaBrowser.Controller/IResourceFileManager.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller
-{
- public interface IResourceFileManager
- {
- string GetResourcePath(string basePath, string virtualPath);
- }
-}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 39b896c0f5..753c18bc7e 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -1,10 +1,10 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
@@ -16,9 +16,7 @@ namespace MediaBrowser.Controller
/// </summary>
public interface IServerApplicationHost : IApplicationHost
{
- event EventHandler HasUpdateAvailableChanged;
-
- IServiceProvider ServiceProvider { get; }
+ bool CoreStartupHasCompleted { get; }
bool CanLaunchWebBrowser { get; }
@@ -40,54 +38,55 @@ namespace MediaBrowser.Controller
bool ListenWithHttps { get; }
/// <summary>
- /// Gets a value indicating whether this instance has update available.
- /// </summary>
- /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
- bool HasUpdateAvailable { get; }
-
- /// <summary>
/// Gets the name of the friendly.
/// </summary>
/// <value>The name of the friendly.</value>
string FriendlyName { get; }
/// <summary>
+ /// Gets the configured published server url.
+ /// </summary>
+ string PublishedServerUrl { get; }
+
+ /// <summary>
/// Gets the system info.
/// </summary>
+ /// <param name="source">The originator of the request.</param>
/// <returns>SystemInfo.</returns>
- Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken);
+ SystemInfo GetSystemInfo(IPAddress source);
- Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken);
+ PublicSystemInfo GetPublicSystemInfo(IPAddress address);
/// <summary>
- /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
- /// to the API that should exist at the address.
+ /// Gets a URL specific for the request.
/// </summary>
- /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
- /// <returns>A list containing all the local IP addresses of the server.</returns>
- Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
+ /// <param name="request">The <see cref="HttpRequest"/> instance.</param>
+ /// <param name="port">Optional port number.</param>
+ /// <returns>An accessible URL.</returns>
+ string GetSmartApiUrl(HttpRequest request, int? port = null);
/// <summary>
- /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
- /// IP address that can be found via <see cref="GetLocalIpAddresses"/>. HTTPS will be preferred when available.
+ /// Gets a URL specific for the request.
/// </summary>
- /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
- /// <returns>The server URL.</returns>
- Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
+ /// <param name="remoteAddr">The remote <see cref="IPAddress"/> of the connection.</param>
+ /// <param name="port">Optional port number.</param>
+ /// <returns>An accessible URL.</returns>
+ string GetSmartApiUrl(IPAddress remoteAddr, int? port = null);
/// <summary>
- /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
- /// over HTTP (not HTTPS).
+ /// Gets a URL specific for the request.
/// </summary>
- /// <returns>The API URL.</returns>
- string GetLoopbackHttpApiUrl();
+ /// <param name="hostname">The hostname used in the connection.</param>
+ /// <param name="port">Optional port number.</param>
+ /// <returns>An accessible URL.</returns>
+ string GetSmartApiUrl(string hostname, int? port = null);
/// <summary>
- /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available.
+ /// Gets a localhost URL that can be used to access the API using the loop-back IP address.
+ /// over HTTP (not HTTPS).
/// </summary>
- /// <param name="address">The IP address to use as the hostname in the URL.</param>
/// <returns>The API URL.</returns>
- string GetLocalApiUrl(IPAddress address);
+ string GetLoopbackHttpApiUrl();
/// <summary>
/// Gets a local (LAN) URL that can be used to access the API.
@@ -103,7 +102,7 @@ namespace MediaBrowser.Controller
/// preferring the HTTPS port, if available.
/// </param>
/// <returns>The API URL.</returns>
- string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null);
+ string GetLocalApiUrl(string hostname, string scheme = null, int? port = null);
/// <summary>
/// Open a URL in an external browser window.
@@ -112,13 +111,10 @@ namespace MediaBrowser.Controller
/// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception>
void LaunchUrl(string url);
- void EnableLoopback(string appName);
-
IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
string ExpandVirtualPath(string path);
- string ReverseVirtualPath(string path);
- Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
+ string ReverseVirtualPath(string path);
}
}
diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs
index be57d6bcae..1890dbb360 100644
--- a/MediaBrowser.Controller/IServerApplicationPaths.cs
+++ b/MediaBrowser.Controller/IServerApplicationPaths.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Common.Configuration;
diff --git a/MediaBrowser.Controller/Library/DeleteOptions.cs b/MediaBrowser.Controller/Library/DeleteOptions.cs
index b7417efcb5..408e702845 100644
--- a/MediaBrowser.Controller/Library/DeleteOptions.cs
+++ b/MediaBrowser.Controller/Library/DeleteOptions.cs
@@ -4,13 +4,13 @@ namespace MediaBrowser.Controller.Library
{
public class DeleteOptions
{
- public bool DeleteFileLocation { get; set; }
-
- public bool DeleteFromExternalProvider { get; set; }
-
public DeleteOptions()
{
DeleteFromExternalProvider = true;
}
+
+ public bool DeleteFileLocation { get; set; }
+
+ public bool DeleteFromExternalProvider { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs
index d45493d404..a74d1b9f0b 100644
--- a/MediaBrowser.Controller/Library/IIntroProvider.cs
+++ b/MediaBrowser.Controller/Library/IIntroProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -10,6 +12,12 @@ namespace MediaBrowser.Controller.Library
public interface IIntroProvider
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
/// Gets the intros.
/// </summary>
/// <param name="item">The item.</param>
@@ -22,11 +30,5 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <returns>IEnumerable{System.String}.</returns>
IEnumerable<string> GetAllIntroFiles();
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- string Name { get; }
}
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index d2f937d4f4..604960d8bc 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -1,9 +1,12 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Naming.Common;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -29,6 +32,29 @@ namespace MediaBrowser.Controller.Library
public interface ILibraryManager
{
/// <summary>
+ /// Occurs when [item added].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemAdded;
+
+ /// <summary>
+ /// Occurs when [item updated].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemUpdated;
+
+ /// <summary>
+ /// Occurs when [item removed].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemRemoved;
+
+ /// <summary>
+ /// Gets the root folder.
+ /// </summary>
+ /// <value>The root folder.</value>
+ AggregateFolder RootFolder { get; }
+
+ bool IsScanRunning { get; }
+
+ /// <summary>
/// Resolves the path.
/// </summary>
/// <param name="fileInfo">The file information.</param>
@@ -41,6 +67,12 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Resolves a set of files into a list of BaseItem.
/// </summary>
+ /// <param name="files">The list of tiles.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ /// <param name="parent">The parent folder.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="collectionType">The collection type.</param>
+ /// <returns>The items resolved from the paths.</returns>
IEnumerable<BaseItem> ResolvePaths(
IEnumerable<FileSystemMetadata> files,
IDirectoryService directoryService,
@@ -49,15 +81,9 @@ namespace MediaBrowser.Controller.Library
string collectionType = null);
/// <summary>
- /// Gets the root folder.
- /// </summary>
- /// <value>The root folder.</value>
- AggregateFolder RootFolder { get; }
-
- /// <summary>
/// Gets a Person.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the person.</param>
/// <returns>Task{Person}.</returns>
Person GetPerson(string name);
@@ -72,29 +98,30 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the artist.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the artist.</param>
/// <returns>Task{Artist}.</returns>
MusicArtist GetArtist(string name);
MusicArtist GetArtist(string name, DtoOptions options);
+
/// <summary>
/// Gets a Studio.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the studio.</param>
/// <returns>Task{Studio}.</returns>
Studio GetStudio(string name);
/// <summary>
/// Gets a Genre.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the genre.</param>
/// <returns>Task{Genre}.</returns>
Genre GetGenre(string name);
/// <summary>
/// Gets the genre.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the music genre.</param>
/// <returns>Task{MusicGenre}.</returns>
MusicGenre GetMusicGenre(string name);
@@ -103,7 +130,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="value">The value.</param>
/// <returns>Task{Year}.</returns>
- /// <exception cref="ArgumentOutOfRangeException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException">Throws if year is invalid.</exception>
Year GetYear(int value);
/// <summary>
@@ -195,16 +222,26 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Creates the item.
/// </summary>
+ /// <param name="item">Item to create.</param>
+ /// <param name="parent">Parent of new item.</param>
void CreateItem(BaseItem item, BaseItem parent);
/// <summary>
/// Creates the items.
/// </summary>
- void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken);
+ /// <param name="items">Items to create.</param>
+ /// <param name="parent">Parent of new items.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken);
/// <summary>
/// Updates the item.
/// </summary>
+ /// <param name="items">Items to update.</param>
+ /// <param name="parent">Parent of updated items.</param>
+ /// <param name="updateReason">Reason for update.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Returns a Task that can be awaited.</returns>
Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
/// <summary>
@@ -214,6 +251,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="parent">The parent item.</param>
/// <param name="updateReason">The update reason.</param>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Returns a Task that can be awaited.</returns>
Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
/// <summary>
@@ -223,22 +261,6 @@ namespace MediaBrowser.Controller.Library
/// <returns>BaseItem.</returns>
BaseItem RetrieveItem(Guid id);
- bool IsScanRunning { get; }
-
- /// <summary>
- /// Occurs when [item added].
- /// </summary>
- event EventHandler<ItemChangeEventArgs> ItemAdded;
-
- /// <summary>
- /// Occurs when [item updated].
- /// </summary>
- event EventHandler<ItemChangeEventArgs> ItemUpdated;
- /// <summary>
- /// Occurs when [item removed].
- /// </summary>
- event EventHandler<ItemChangeEventArgs> ItemRemoved;
-
/// <summary>
/// Finds the type of the collection.
/// </summary>
@@ -283,16 +305,25 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Deletes the item.
/// </summary>
+ /// <param name="item">Item to delete.</param>
+ /// <param name="options">Options to use for deletion.</param>
void DeleteItem(BaseItem item, DeleteOptions options);
/// <summary>
/// Deletes the item.
/// </summary>
+ /// <param name="item">Item to delete.</param>
+ /// <param name="options">Options to use for deletion.</param>
+ /// <param name="notifyParentItem">Notify parent of deletion.</param>
void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem);
/// <summary>
/// Deletes the item.
/// </summary>
+ /// <param name="item">Item to delete.</param>
+ /// <param name="options">Options to use for deletion.</param>
+ /// <param name="parent">Parent of item.</param>
+ /// <param name="notifyParentItem">Notify parent of deletion.</param>
void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem);
/// <summary>
@@ -303,6 +334,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="parentId">The parent identifier.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
User user,
string name,
@@ -317,6 +349,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="name">The name.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
User user,
string name,
@@ -329,6 +362,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="name">The name.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
string name,
string viewType,
@@ -342,6 +376,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,
@@ -355,10 +390,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].
@@ -384,6 +420,9 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Fills the missing episode numbers from path.
/// </summary>
+ /// <param name="episode">Episode to use.</param>
+ /// <param name="forceRefresh">Option to force refresh of episode numbers.</param>
+ /// <returns>True if successful.</returns>
bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh);
/// <summary>
@@ -465,6 +504,15 @@ namespace MediaBrowser.Controller.Library
void UpdatePeople(BaseItem item, List<PersonInfo> people);
/// <summary>
+ /// Asynchronously updates the people.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="people">The people.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The async task.</returns>
+ Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken);
+
+ /// <summary>
/// Gets the item ids.
/// </summary>
/// <param name="query">The query.</param>
@@ -517,6 +565,9 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the items.
/// </summary>
+ /// <param name="query">The query to use.</param>
+ /// <param name="parents">Items to use for query.</param>
+ /// <returns>List of items.</returns>
List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
/// <summary>
@@ -540,7 +591,7 @@ namespace MediaBrowser.Controller.Library
Guid GetMusicGenreId(string name);
- Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary);
+ Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary);
Task RemoveVirtualFolder(string name, bool refreshLibrary);
@@ -564,8 +615,21 @@ namespace MediaBrowser.Controller.Library
int GetCount(InternalItemsQuery query);
- void AddExternalSubtitleStreams(List<MediaStream> streams,
+ void AddExternalSubtitleStreams(
+ List<MediaStream> streams,
string videoPath,
string[] files);
+
+ Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
+
+ BaseItem GetParentItem(string parentId, Guid? userId);
+
+ BaseItem GetParentItem(Guid? parentId, Guid? userId);
+
+ /// <summary>
+ /// Gets or creates a static instance of <see cref="NamingOptions"/>.
+ /// </summary>
+ /// <returns>An instance of the <see cref="NamingOptions"/> class.</returns>
+ NamingOptions GetNamingOptions();
}
}
diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs
index ff25be6577..323aa48768 100644
--- a/MediaBrowser.Controller/Library/ILiveStream.cs
+++ b/MediaBrowser.Controller/Library/ILiveStream.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1711, CS1591
using System.Threading;
using System.Threading.Tasks;
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 9e7b1e6085..fd3631da9c 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CS1591
using System;
using System.Collections.Generic;
@@ -28,12 +30,14 @@ namespace MediaBrowser.Controller.Library
/// <param name="itemId">The item identifier.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
List<MediaStream> GetMediaStreams(Guid itemId);
+
/// <summary>
/// Gets the media streams.
/// </summary>
/// <param name="mediaSourceId">The media source identifier.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
List<MediaStream> GetMediaStreams(string mediaSourceId);
+
/// <summary>
/// Gets the media streams.
/// </summary>
@@ -58,16 +62,32 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the playack media sources.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="user">User to use for operation.</param>
+ /// <param name="allowMediaProbe">Option to allow media probe.</param>
+ /// <param name="enablePathSubstitution">Option to enable path substitution.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>List of media sources wrapped in an awaitable task.</returns>
Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
/// <summary>
/// Gets the static media sources.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="enablePathSubstitution">Option to enable path substitution.</param>
+ /// <param name="user">User to use for operation.</param>
+ /// <returns>List of media sources.</returns>
List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null);
/// <summary>
/// Gets the static media source.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="mediaSourceId">Media source to get.</param>
+ /// <param name="liveStreamId">Live stream to use.</param>
+ /// <param name="enablePathSubstitution">Option to enable path substitution.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>The static media source wrapped in an awaitable task.</returns>
Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken);
/// <summary>
@@ -113,5 +133,7 @@ namespace MediaBrowser.Controller.Library
public interface IDirectStreamProvider
{
Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
+
+ string GetFilePath();
}
}
diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs
index 5bf4acebb4..ca4b53fbee 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CS1591
using System.Collections.Generic;
using System.Threading;
@@ -21,6 +21,10 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Opens the media source.
/// </summary>
+ /// <param name="openToken">Token to use.</param>
+ /// <param name="currentLiveStreams">List of live streams.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>The media source wrapped as an awaitable task.</returns>
Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Library/IMetadataSaver.cs b/MediaBrowser.Controller/Library/IMetadataSaver.cs
index 027cc5b40e..d963fd2491 100644
--- a/MediaBrowser.Controller/Library/IMetadataSaver.cs
+++ b/MediaBrowser.Controller/Library/IMetadataSaver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Threading;
using MediaBrowser.Controller.Entities;
@@ -27,7 +29,6 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
void Save(BaseItem item, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
index d12f008e77..ec34a868b3 100644
--- a/MediaBrowser.Controller/Library/IMusicManager.cs
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CS1591
using System.Collections.Generic;
using Jellyfin.Data.Entities;
@@ -13,16 +15,28 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the instant mix from song.
/// </summary>
+ /// <param name="item">The item to use.</param>
+ /// <param name="user">The user to use.</param>
+ /// <param name="dtoOptions">The options to use.</param>
+ /// <returns>List of items.</returns>
List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from artist.
/// </summary>
+ /// <param name="artist">The artist to use.</param>
+ /// <param name="user">The user to use.</param>
+ /// <param name="dtoOptions">The options to use.</param>
+ /// <returns>List of items.</returns>
List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from genre.
/// </summary>
+ /// <param name="genres">The genres to use.</param>
+ /// <param name="user">The user to use.</param>
+ /// <param name="dtoOptions">The options to use.</param>
+ /// <returns>List of items.</returns>
List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions);
}
}
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index c6a83e4dc4..cf35b48dba 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CA1707, CS1591
using System;
using System.Collections.Generic;
@@ -40,6 +42,9 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the user data dto.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="user">User to use.</param>
+ /// <returns>User data dto.</returns>
UserItemDataDto GetUserDataDto(BaseItem item, User user);
UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions dto_options);
@@ -47,22 +52,25 @@ 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>
/// Updates playstate for an item and returns true or false indicating if it was played to completion.
/// </summary>
+ /// <param name="item">Item to update.</param>
+ /// <param name="data">Data to update.</param>
+ /// <param name="positionTicks">New playstate.</param>
+ /// <returns>True if playstate was updated.</returns>
bool UpdatePlayState(BaseItem item, UserItemData data, long? positionTicks);
}
}
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 6a4f5cf679..21776f8915 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -36,6 +38,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Initializes the user manager and ensures that a user exists.
/// </summary>
+ /// <returns>Awaitable task.</returns>
Task InitializeAsync();
/// <summary>
@@ -59,16 +62,16 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <param name="newName">The new name.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
Task RenameUser(User user, string newName);
/// <summary>
/// Updates the user.
/// </summary>
/// <param name="user">The user.</param>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
void UpdateUser(User user);
/// <summary>
@@ -85,15 +88,16 @@ 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>
/// Deletes the specified user.
/// </summary>
/// <param name="userId">The id of the user to be deleted.</param>
- void DeleteUser(Guid userId);
+ /// <returns>A task representing the deletion of the user.</returns>
+ Task DeleteUserAsync(Guid userId);
/// <summary>
/// Resets the password.
@@ -106,17 +110,22 @@ namespace MediaBrowser.Controller.Library
/// Resets the easy password.
/// </summary>
/// <param name="user">The user.</param>
- /// <returns>Task.</returns>
void ResetEasyPassword(User user);
/// <summary>
/// Changes the password.
/// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="newPassword">New password to use.</param>
+ /// <returns>Awaitable task.</returns>
Task ChangePassword(User user, string newPassword);
/// <summary>
/// Changes the easy password.
/// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="newPassword">New password to use.</param>
+ /// <param name="newPasswordSha1">Hash of new password.</param>
void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1);
/// <summary>
@@ -130,6 +139,12 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Authenticates the user.
/// </summary>
+ /// <param name="username">The user.</param>
+ /// <param name="password">The password to use.</param>
+ /// <param name="passwordSha1">Hash of password.</param>
+ /// <param name="remoteEndPoint">Remove endpoint to use.</param>
+ /// <param name="isUserSession">Specifies if a user session.</param>
+ /// <returns>User wrapped in awaitable task.</returns>
Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
/// <summary>
@@ -158,7 +173,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="userId">The user's Id.</param>
/// <param name="config">The request containing the new user configuration.</param>
- void UpdateConfiguration(Guid userId, UserConfiguration config);
+ /// <returns>A task representing the update.</returns>
+ Task UpdateConfigurationAsync(Guid userId, UserConfiguration config);
/// <summary>
/// This method updates the user's policy.
@@ -167,12 +183,14 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="userId">The user's Id.</param>
/// <param name="policy">The request containing the new user policy.</param>
- void UpdatePolicy(Guid userId, UserPolicy policy);
+ /// <returns>A task representing the update.</returns>
+ Task UpdatePolicyAsync(Guid userId, UserPolicy policy);
/// <summary>
/// Clears the user's profile image.
/// </summary>
/// <param name="user">The user.</param>
- void ClearProfileImage(User user);
+ /// <returns>A task representing the clearing of the profile image.</returns>
+ Task ClearProfileImageAsync(User user);
}
}
diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs
index 8d541e8b68..055627d3e3 100644
--- a/MediaBrowser.Controller/Library/IUserViewManager.cs
+++ b/MediaBrowser.Controller/Library/IUserViewManager.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CS1591
using System;
using System.Collections.Generic;
@@ -11,10 +13,29 @@ namespace MediaBrowser.Controller.Library
{
public interface IUserViewManager
{
+ /// <summary>
+ /// Gets user views.
+ /// </summary>
+ /// <param name="query">Query to use.</param>
+ /// <returns>Set of folders.</returns>
Folder[] GetUserViews(UserViewQuery query);
+ /// <summary>
+ /// Gets user sub views.
+ /// </summary>
+ /// <param name="parentId">Parent to use.</param>
+ /// <param name="type">Type to use.</param>
+ /// <param name="localizationKey">Localization key to use.</param>
+ /// <param name="sortName">Sort to use.</param>
+ /// <returns>User view.</returns>
UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName);
+ /// <summary>
+ /// Gets latest items.
+ /// </summary>
+ /// <param name="request">Query to use.</param>
+ /// <param name="options">Options to use.</param>
+ /// <returns>Set of items.</returns>
List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options);
}
}
diff --git a/MediaBrowser.Controller/Library/IntroInfo.cs b/MediaBrowser.Controller/Library/IntroInfo.cs
index 283cc631ca..90786786b2 100644
--- a/MediaBrowser.Controller/Library/IntroInfo.cs
+++ b/MediaBrowser.Controller/Library/IntroInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
index 1798a4fada..3586dc69d6 100644
--- a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1711, CS1591
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 6a0dbeba2f..bfc1e4857f 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1721, CA1819, CS1591
using System;
using System.Collections.Generic;
@@ -14,14 +16,14 @@ namespace MediaBrowser.Controller.Library
/// These are arguments relating to the file system that are collected once and then referred to
/// whenever needed. Primarily for entity resolution.
/// </summary>
- public class ItemResolveArgs : EventArgs
+ public class ItemResolveArgs
{
/// <summary>
/// The _app paths.
/// </summary>
private readonly IServerApplicationPaths _appPaths;
- public IDirectoryService DirectoryService { get; private set; }
+ private LibraryOptions _libraryOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ItemResolveArgs" /> class.
@@ -34,17 +36,18 @@ namespace MediaBrowser.Controller.Library
DirectoryService = directoryService;
}
+ public IDirectoryService DirectoryService { get; }
+
/// <summary>
- /// Gets the file system children.
+ /// Gets or sets the file system children.
/// </summary>
/// <value>The file system children.</value>
public FileSystemMetadata[] FileSystemChildren { get; set; }
- public LibraryOptions LibraryOptions { get; set; }
-
- public LibraryOptions GetLibraryOptions()
+ public LibraryOptions LibraryOptions
{
- return LibraryOptions ?? (LibraryOptions = Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent));
+ get => _libraryOptions ??= Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent);
+ set => _libraryOptions = value;
}
/// <summary>
@@ -60,10 +63,10 @@ namespace MediaBrowser.Controller.Library
public FileSystemMetadata FileInfo { get; set; }
/// <summary>
- /// Gets or sets the path.
+ /// Gets the path.
/// </summary>
/// <value>The path.</value>
- public string Path { get; set; }
+ public string Path => FileInfo.FullName;
/// <summary>
/// Gets a value indicating whether this instance is directory.
@@ -106,6 +109,21 @@ namespace MediaBrowser.Controller.Library
/// <value>The additional locations.</value>
private List<string> AdditionalLocations { get; set; }
+ /// <summary>
+ /// Gets the physical locations.
+ /// </summary>
+ /// <value>The physical locations.</value>
+ public string[] PhysicalLocations
+ {
+ get
+ {
+ var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path };
+ return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray();
+ }
+ }
+
+ public string CollectionType { get; set; }
+
public bool HasParent<T>()
where T : Folder
{
@@ -136,10 +154,20 @@ namespace MediaBrowser.Controller.Library
}
/// <summary>
+ /// Determines whether the specified <see cref="object" /> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The object to compare with the current object.</param>
+ /// <returns><c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as ItemResolveArgs);
+ }
+
+ /// <summary>
/// Adds the additional location.
/// </summary>
/// <param name="path">The path.</param>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
public void AddAdditionalLocation(string path)
{
if (string.IsNullOrEmpty(path))
@@ -147,34 +175,18 @@ namespace MediaBrowser.Controller.Library
throw new ArgumentException("The path was empty or null.", nameof(path));
}
- if (AdditionalLocations == null)
- {
- AdditionalLocations = new List<string>();
- }
-
+ AdditionalLocations ??= new List<string>();
AdditionalLocations.Add(path);
}
// REVIEW: @bond
- /// <summary>
- /// Gets the physical locations.
- /// </summary>
- /// <value>The physical locations.</value>
- public string[] PhysicalLocations
- {
- get
- {
- var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path };
- return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray();
- }
- }
/// <summary>
/// Gets the name of the file system entry by.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>FileSystemInfo.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception>
public FileSystemMetadata GetFileSystemEntryByName(string name)
{
if (string.IsNullOrEmpty(name))
@@ -190,7 +202,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="path">The path.</param>
/// <returns>FileSystemInfo.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if path is invalid.</exception>
public FileSystemMetadata GetFileSystemEntryByPath(string path)
{
if (string.IsNullOrEmpty(path))
@@ -224,32 +236,20 @@ namespace MediaBrowser.Controller.Library
return CollectionType;
}
- public string CollectionType { get; set; }
-
- /// <summary>
- /// Determines whether the specified <see cref="object" /> is equal to this instance.
- /// </summary>
- /// <param name="obj">The object to compare with the current object.</param>
- /// <returns><c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
- public override bool Equals(object obj)
- {
- return Equals(obj as ItemResolveArgs);
- }
-
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
public override int GetHashCode()
{
- return Path.GetHashCode();
+ return Path.GetHashCode(StringComparison.Ordinal);
}
/// <summary>
/// Equals the specified args.
/// </summary>
/// <param name="args">The args.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <returns><c>true</c> if the arguments are the same, <c>false</c> otherwise.</returns>
protected bool Equals(ItemResolveArgs args)
{
if (args != null)
diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
index 9581603f04..7bc8fa5abd 100644
--- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
+++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs
new file mode 100644
index 0000000000..41cfcae163
--- /dev/null
+++ b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs
@@ -0,0 +1,17 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Library
+{
+ public static class MetadataConfigurationExtensions
+ {
+ public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config)
+ {
+ return config.GetConfiguration<MetadataConfiguration>("metadata");
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs
index f16304db01..a6be6c0d3c 100644
--- a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs
+++ b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs
@@ -14,18 +14,10 @@ namespace MediaBrowser.Controller.Library
{
new ConfigurationStore
{
- Key = "metadata",
- ConfigurationType = typeof(MetadataConfiguration)
+ Key = "metadata",
+ ConfigurationType = typeof(MetadataConfiguration)
}
};
}
}
-
- public static class MetadataConfigurationExtensions
- {
- public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config)
- {
- return config.GetConfiguration<MetadataConfiguration>("metadata");
- }
- }
}
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
index 21f33ad190..a49dcacc1d 100644
--- a/MediaBrowser.Controller/Library/NameExtensions.cs
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -3,13 +3,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Extensions;
namespace MediaBrowser.Controller.Library
{
public static class NameExtensions
{
- private static string RemoveDiacritics(string name)
+ public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
+ => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
+ .Select(x => x.First());
+
+ private static string RemoveDiacritics(string? name)
{
if (name == null)
{
@@ -18,9 +23,5 @@ namespace MediaBrowser.Controller.Library
return name.RemoveDiacritics();
}
-
- public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
- => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
- .Select(x => x.First());
}
}
diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
index a2be3a42ab..76e9eb1f54 100644
--- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
+++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs
index ac372bceba..2138fef580 100644
--- a/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs
+++ b/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs
@@ -1,4 +1,4 @@
-namespace MediaBrowser.Controller.Library
+namespace MediaBrowser.Controller.Library
{
/// <summary>
/// An event that occurs when playback is started.
diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs
deleted file mode 100644
index 5efdc6a481..0000000000
--- a/MediaBrowser.Controller/Library/Profiler.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Globalization;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Controller.Library
-{
- /// <summary>
- /// Class Profiler.
- /// </summary>
- public class Profiler : IDisposable
- {
- /// <summary>
- /// The name.
- /// </summary>
- readonly string _name;
-
- /// <summary>
- /// The stopwatch.
- /// </summary>
- readonly Stopwatch _stopwatch;
-
- /// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger<Profiler> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="Profiler" /> class.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <param name="logger">The logger.</param>
- public Profiler(string name, ILogger<Profiler> logger)
- {
- this._name = name;
-
- _logger = logger;
-
- _stopwatch = new Stopwatch();
- _stopwatch.Start();
- }
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- _stopwatch.Stop();
- string message;
- if (_stopwatch.ElapsedMilliseconds > 300000)
- {
- message = string.Format(
- CultureInfo.InvariantCulture,
- "{0} took {1} minutes.",
- _name,
- ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F", CultureInfo.InvariantCulture));
- }
- else
- {
- message = string.Format(
- CultureInfo.InvariantCulture,
- "{0} took {1} seconds.",
- _name,
- ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000", CultureInfo.InvariantCulture));
- }
-
- _logger.LogInformation(message);
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Library/SearchHintInfo.cs b/MediaBrowser.Controller/Library/SearchHintInfo.cs
index 897c2b7f49..de7806adc3 100644
--- a/MediaBrowser.Controller/Library/SearchHintInfo.cs
+++ b/MediaBrowser.Controller/Library/SearchHintInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Library
diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs
index a3aa6019ec..968338dc6a 100644
--- a/MediaBrowser.Controller/Library/TVUtils.cs
+++ b/MediaBrowser.Controller/Library/TVUtils.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
namespace MediaBrowser.Controller.Library
{
@@ -12,7 +13,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="day">The day.</param>
/// <returns>List{DayOfWeek}.</returns>
- public static DayOfWeek[] GetAirDays(string day)
+ [return: NotNullIfNotNull("day")]
+ public static DayOfWeek[]? GetAirDays(string? day)
{
if (!string.IsNullOrEmpty(day))
{
diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
index cd91097535..4d90346f29 100644
--- a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
+++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs b/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs
new file mode 100644
index 0000000000..463061e686
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs
@@ -0,0 +1,19 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Threading;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class ActiveRecordingInfo
+ {
+ public string Id { get; set; }
+
+ public string Path { get; set; }
+
+ public TimerInfo Timer { get; set; }
+
+ public CancellationTokenSource CancellationTokenSource { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
index d7afd21184..699c15f934 100644
--- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.LiveTv;
@@ -22,7 +24,7 @@ namespace MediaBrowser.Controller.LiveTv
public string Number { get; set; }
/// <summary>
- /// Get or sets the Id.
+ /// Gets or sets the Id.
/// </summary>
/// <value>The id of the channel.</value>
public string Id { get; set; }
@@ -46,13 +48,19 @@ namespace MediaBrowser.Controller.LiveTv
public ChannelType ChannelType { get; set; }
/// <summary>
- /// Supply the image path if it can be accessed directly from the file system.
+ /// Gets or sets the group of the channel.
+ /// </summary>
+ /// <value>The group of the channel.</value>
+ public string ChannelGroup { get; set; }
+
+ /// <summary>
+ /// Gets or sets the the image path if it can be accessed directly from the file system.
/// </summary>
/// <value>The image path.</value>
public string ImagePath { get; set; }
/// <summary>
- /// Supply the image url if it can be downloaded.
+ /// Gets or sets the image url if it can be downloaded.
/// </summary>
/// <value>The image URL.</value>
public string ImageUrl { get; set; }
@@ -62,6 +70,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
public bool? HasImage { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether this instance is favorite.
/// </summary>
diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs
index 038ff2eaeb..2bd4b20e8c 100644
--- a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs
+++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index 55c3309317..bd097c90ac 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -20,12 +22,22 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
public interface ILiveTvManager
{
+ event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
+
+ event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
+
+ event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
+
+ event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
+
/// <summary>
/// Gets the services.
/// </summary>
/// <value>The services.</value>
IReadOnlyList<ILiveTvService> Services { get; }
+ IListingsProvider[] ListingProviders { get; }
+
/// <summary>
/// Gets the new timer defaults asynchronous.
/// </summary>
@@ -84,6 +96,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
+ /// <returns>A recording.</returns>
QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options);
/// <summary>
@@ -174,11 +187,16 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Recommended programs.</returns>
QueryResult<BaseItemDto> GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken);
/// <summary>
/// Gets the recommended programs internal.
/// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Recommended programs.</returns>
QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken);
/// <summary>
@@ -200,6 +218,7 @@ namespace MediaBrowser.Controller.LiveTv
/// Gets the live tv folder.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Live TV folder.</returns>
Folder GetInternalLiveTvFolder(CancellationToken cancellationToken);
/// <summary>
@@ -211,11 +230,18 @@ namespace MediaBrowser.Controller.LiveTv
/// <summary>
/// Gets the internal channels.
/// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="dtoOptions">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Internal channels.</returns>
QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken);
/// <summary>
/// Gets the channel media sources.
/// </summary>
+ /// <param name="item">Item to search for.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Channel media sources wrapped in a task.</returns>
Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken);
/// <summary>
@@ -225,12 +251,16 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="fields">The fields.</param>
/// <param name="user">The user.</param>
/// <returns>Task.</returns>
- Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, ItemFields[] fields, User user = null);
+ Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
/// <summary>
/// Saves the tuner host.
/// </summary>
+ /// <param name="info">Turner host to save.</param>
+ /// <param name="dataSourceChanged">Option to specify that data source has changed.</param>
+ /// <returns>Tuner host information wrapped in a task.</returns>
Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
+
/// <summary>
/// Saves the listing provider.
/// </summary>
@@ -265,17 +295,12 @@ namespace MediaBrowser.Controller.LiveTv
void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user);
Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
- Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
- IListingsProvider[] ListingProviders { get; }
+ Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
List<NameIdPair> GetTunerHostTypes();
- Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
- event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
- event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
- event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
- event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
+ Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
string GetEmbyTvActiveRecordingPath(string id);
@@ -285,15 +310,4 @@ namespace MediaBrowser.Controller.LiveTv
List<BaseItem> GetRecordingFolders(User user);
}
-
- public class ActiveRecordingInfo
- {
- public string Id { get; set; }
-
- public string Path { get; set; }
-
- public TimerInfo Timer { get; set; }
-
- public CancellationTokenSource CancellationTokenSource { get; set; }
- }
}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
index 3ca1d165ef..897f263f3d 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
index ff92bf856c..24820abb90 100644
--- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs
+++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -28,6 +30,8 @@ namespace MediaBrowser.Controller.LiveTv
/// <summary>
/// Gets the channels.
/// </summary>
+ /// <param name="enableCache">Option to enable using cache.</param>
+ /// <param name="cancellationToken">The CancellationToken for this operation.</param>
/// <returns>Task&lt;IEnumerable&lt;ChannelInfo&gt;&gt;.</returns>
Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken);
@@ -45,6 +49,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="streamId">The stream identifier.</param>
/// <param name="currentLiveStreams">The current live streams.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>Live stream wrapped in a task.</returns>
Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
/// <summary>
@@ -56,7 +61,6 @@ namespace MediaBrowser.Controller.LiveTv
Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken);
Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken);
-
}
public interface IConfigurableTunerHost
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index ec933caf34..074e023e8d 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -16,23 +18,6 @@ namespace MediaBrowser.Controller.LiveTv
{
public class LiveTvChannel : BaseItem, IHasMediaSources, IHasProgramAttributes
{
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- if (!ConfigurationManager.Configuration.DisableLiveTvChannelUserDataName)
- {
- list.Insert(0, GetClientTypeName() + "-" + Name);
- }
-
- return list;
- }
-
- public override UnratedItem GetBlockUnratedType()
- {
- return UnratedItem.LiveTvChannel;
- }
-
[JsonIgnore]
public override bool SupportsPositionTicksResume => false;
@@ -57,6 +42,67 @@ namespace MediaBrowser.Controller.LiveTv
[JsonIgnore]
public override LocationType LocationType => LocationType.Remote;
+ [JsonIgnore]
+ public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
+
+ [JsonIgnore]
+ public bool IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
+ [JsonIgnore]
+ public bool IsSports { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is series.
+ /// </summary>
+ /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
+ [JsonIgnore]
+ public bool IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is news.
+ /// </summary>
+ /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
+ [JsonIgnore]
+ public bool IsNews { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
+ [JsonIgnore]
+ public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
+
+ [JsonIgnore]
+ public bool IsRepeat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ /// <value>The episode title.</value>
+ [JsonIgnore]
+ public string EpisodeTitle { get; set; }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ if (!ConfigurationManager.Configuration.DisableLiveTvChannelUserDataName)
+ {
+ list.Insert(0, GetClientTypeName() + "-" + Name);
+ }
+
+ return list;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.LiveTvChannel;
+ }
+
protected override string CreateSortName()
{
if (!string.IsNullOrEmpty(Number))
@@ -72,15 +118,12 @@ namespace MediaBrowser.Controller.LiveTv
return (Number ?? string.Empty) + "-" + (Name ?? string.Empty);
}
- [JsonIgnore]
- public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
-
public override string GetClientTypeName()
{
return "TvChannel";
}
- public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+ public IEnumerable<BaseItem> GetTaggedItems()
{
return new List<BaseItem>();
}
@@ -120,46 +163,5 @@ namespace MediaBrowser.Controller.LiveTv
{
return false;
}
-
- [JsonIgnore]
- public bool IsMovie { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is sports.
- /// </summary>
- /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public bool IsSports { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is series.
- /// </summary>
- /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public bool IsSeries { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is news.
- /// </summary>
- /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public bool IsNews { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is kids.
- /// </summary>
- /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
-
- [JsonIgnore]
- public bool IsRepeat { get; set; }
-
- /// <summary>
- /// Gets or sets the episode title.
- /// </summary>
- /// <value>The episode title.</value>
- [JsonIgnore]
- public string EpisodeTitle { get; set; }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 43af495dd6..111dc0d275 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS1591, SA1306
using System;
using System.Collections.Generic;
@@ -17,59 +19,20 @@ namespace MediaBrowser.Controller.LiveTv
{
public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes
{
+ private static string EmbyServiceName = "Emby";
+
public LiveTvProgram()
{
IsVirtualItem = true;
}
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- if (!IsSeries)
- {
- var key = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(key))
- {
- list.Insert(0, key);
- }
-
- key = this.GetProviderId(MetadataProvider.Tmdb);
- if (!string.IsNullOrEmpty(key))
- {
- list.Insert(0, key);
- }
- }
- else if (!string.IsNullOrEmpty(EpisodeTitle))
- {
- var name = GetClientTypeName();
-
- list.Insert(0, name + "-" + Name + (EpisodeTitle ?? string.Empty));
- }
-
- return list;
- }
-
- private static string EmbyServiceName = "Emby";
- public override double GetDefaultPrimaryImageAspectRatio()
- {
- var serviceName = ServiceName;
-
- if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase))
- {
- return 2.0 / 3;
- }
- else
- {
- return 16.0 / 9;
- }
- }
+ public string SeriesName { get; set; }
[JsonIgnore]
public override SourceType SourceType => SourceType.LiveTV;
/// <summary>
- /// The start date of the program, in UTC.
+ /// Gets or sets start date of the program, in UTC.
/// </summary>
[JsonIgnore]
public DateTime StartDate { get; set; }
@@ -99,7 +62,7 @@ namespace MediaBrowser.Controller.LiveTv
public bool IsMovie { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether this instance is sports.
+ /// Gets a value indicating whether this instance is sports.
/// </summary>
/// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
[JsonIgnore]
@@ -113,49 +76,49 @@ namespace MediaBrowser.Controller.LiveTv
public bool IsSeries { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether this instance is live.
+ /// Gets a value indicating whether this instance is live.
/// </summary>
/// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
[JsonIgnore]
public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Gets or sets a value indicating whether this instance is news.
+ /// Gets a value indicating whether this instance is news.
/// </summary>
/// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
[JsonIgnore]
public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Gets or sets a value indicating whether this instance is kids.
+ /// Gets a value indicating whether this instance is kids.
/// </summary>
/// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
[JsonIgnore]
public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Gets or sets a value indicating whether this instance is premiere.
+ /// Gets a value indicating whether this instance is premiere.
/// </summary>
/// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
[JsonIgnore]
public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Returns the folder containing the item.
+ /// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
/// </summary>
/// <value>The containing folder path.</value>
[JsonIgnore]
public override string ContainingFolderPath => Path;
- //[JsonIgnore]
+ // [JsonIgnore]
// public override string MediaType
- //{
+ // {
// get
// {
// return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio;
// }
- //}
+ // }
[JsonIgnore]
public bool IsAiring
@@ -179,6 +142,66 @@ namespace MediaBrowser.Controller.LiveTv
}
}
+ [JsonIgnore]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ // Optimization
+ if (IsNews || IsSports)
+ {
+ return false;
+ }
+
+ return base.SupportsPeople;
+ }
+ }
+
+ [JsonIgnore]
+ public override bool SupportsAncestors => false;
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ if (!IsSeries)
+ {
+ var key = this.GetProviderId(MetadataProvider.Imdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+
+ key = this.GetProviderId(MetadataProvider.Tmdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+ }
+ else if (!string.IsNullOrEmpty(EpisodeTitle))
+ {
+ var name = GetClientTypeName();
+
+ list.Insert(0, name + "-" + Name + (EpisodeTitle ?? string.Empty));
+ }
+
+ return list;
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ var serviceName = ServiceName;
+
+ if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase))
+ {
+ return 2.0 / 3;
+ }
+ else
+ {
+ return 16.0 / 9;
+ }
+ }
+
public override string GetClientTypeName()
{
return "Program";
@@ -199,24 +222,6 @@ namespace MediaBrowser.Controller.LiveTv
return false;
}
- [JsonIgnore]
- public override bool SupportsPeople
- {
- get
- {
- // Optimization
- if (IsNews || IsSports)
- {
- return false;
- }
-
- return base.SupportsPeople;
- }
- }
-
- [JsonIgnore]
- public override bool SupportsAncestors => false;
-
private LiveTvOptions GetConfiguration()
{
return ConfigurationManager.GetConfiguration<LiveTvOptions>("livetv");
@@ -270,7 +275,5 @@ namespace MediaBrowser.Controller.LiveTv
return list;
}
-
- public string SeriesName { get; set; }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
index 02178297b1..eb3babc180 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -42,6 +44,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The tuners.</value>
public List<LiveTvTunerInfo> Tuners { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether this instance is visible.
/// </summary>
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
index 739978e7ce..aa5eb59d16 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
index bdcffd5cae..3c3ac2471f 100644
--- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,8 +10,16 @@ namespace MediaBrowser.Controller.LiveTv
{
public class ProgramInfo
{
+ public ProgramInfo()
+ {
+ Genres = new List<string>();
+
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
/// <summary>
- /// Id of the program.
+ /// Gets or sets the id of the program.
/// </summary>
public string Id { get; set; }
@@ -20,7 +30,7 @@ namespace MediaBrowser.Controller.LiveTv
public string ChannelId { get; set; }
/// <summary>
- /// Name of the program.
+ /// Gets or sets the name of the program.
/// </summary>
public string Name { get; set; }
@@ -35,6 +45,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The overview.</value>
public string Overview { get; set; }
+
/// <summary>
/// Gets or sets the short overview.
/// </summary>
@@ -42,17 +53,17 @@ namespace MediaBrowser.Controller.LiveTv
public string ShortOverview { get; set; }
/// <summary>
- /// The start date of the program, in UTC.
+ /// Gets or sets the start date of the program, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the program, in UTC.
+ /// Gets or sets the end date of the program, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
/// <summary>
- /// Genre of the program.
+ /// Gets or sets the genre of the program.
/// </summary>
public List<string> Genres { get; set; }
@@ -68,6 +79,9 @@ namespace MediaBrowser.Controller.LiveTv
/// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value>
public bool? IsHD { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is 3d.
+ /// </summary>
public bool? Is3D { get; set; }
/// <summary>
@@ -97,13 +111,13 @@ namespace MediaBrowser.Controller.LiveTv
public string EpisodeTitle { get; set; }
/// <summary>
- /// Supply the image path if it can be accessed directly from the file system.
+ /// Gets or sets the image path if it can be accessed directly from the file system.
/// </summary>
/// <value>The image path.</value>
public string ImagePath { get; set; }
/// <summary>
- /// Supply the image url if it can be downloaded.
+ /// Gets or sets the image url if it can be downloaded.
/// </summary>
/// <value>The image URL.</value>
public string ImageUrl { get; set; }
@@ -169,31 +183,37 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The production year.</value>
public int? ProductionYear { get; set; }
+
/// <summary>
/// Gets or sets the home page URL.
/// </summary>
/// <value>The home page URL.</value>
public string HomePageUrl { get; set; }
+
/// <summary>
/// Gets or sets the series identifier.
/// </summary>
/// <value>The series identifier.</value>
public string SeriesId { get; set; }
+
/// <summary>
/// Gets or sets the show identifier.
/// </summary>
/// <value>The show identifier.</value>
public string ShowId { 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 episode number.
/// </summary>
/// <value>The episode number.</value>
public int? EpisodeNumber { get; set; }
+
/// <summary>
/// Gets or sets the etag.
/// </summary>
@@ -203,13 +223,5 @@ namespace MediaBrowser.Controller.LiveTv
public Dictionary<string, string> ProviderIds { get; set; }
public Dictionary<string, string> SeriesProviderIds { get; set; }
-
- public ProgramInfo()
- {
- Genres = new List<string>();
-
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
index 303882b7ef..1dcf7a58fe 100644
--- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,8 +10,13 @@ namespace MediaBrowser.Controller.LiveTv
{
public class RecordingInfo
{
+ public RecordingInfo()
+ {
+ Genres = new List<string>();
+ }
+
/// <summary>
- /// Id of the recording.
+ /// Gets or sets the id of the recording.
/// </summary>
public string Id { get; set; }
@@ -26,7 +33,7 @@ namespace MediaBrowser.Controller.LiveTv
public string TimerId { get; set; }
/// <summary>
- /// ChannelId of the recording.
+ /// Gets or sets the channelId of the recording.
/// </summary>
public string ChannelId { get; set; }
@@ -37,7 +44,7 @@ namespace MediaBrowser.Controller.LiveTv
public ChannelType ChannelType { get; set; }
/// <summary>
- /// Name of the recording.
+ /// Gets or sets the name of the recording.
/// </summary>
public string Name { get; set; }
@@ -60,12 +67,12 @@ namespace MediaBrowser.Controller.LiveTv
public string Overview { get; set; }
/// <summary>
- /// The start date of the recording, in UTC.
+ /// Gets or sets the start date of the recording, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the recording, in UTC.
+ /// Gets or sets the end date of the recording, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
@@ -82,7 +89,7 @@ namespace MediaBrowser.Controller.LiveTv
public RecordingStatus Status { get; set; }
/// <summary>
- /// Genre of the program.
+ /// Gets or sets the genre of the program.
/// </summary>
public List<string> Genres { get; set; }
@@ -171,13 +178,13 @@ namespace MediaBrowser.Controller.LiveTv
public float? CommunityRating { get; set; }
/// <summary>
- /// Supply the image path if it can be accessed directly from the file system.
+ /// Gets or sets the image path if it can be accessed directly from the file system.
/// </summary>
/// <value>The image path.</value>
public string ImagePath { get; set; }
/// <summary>
- /// Supply the image url if it can be downloaded.
+ /// Gets or sets the image url if it can be downloaded.
/// </summary>
/// <value>The image URL.</value>
public string ImageUrl { get; set; }
@@ -187,6 +194,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
public bool? HasImage { get; set; }
+
/// <summary>
/// Gets or sets the show identifier.
/// </summary>
@@ -198,10 +206,5 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The date last updated.</value>
public DateTime DateLastUpdated { get; set; }
-
- public RecordingInfo()
- {
- Genres = new List<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
index 847c0ea8c0..0b943c9396 100644
--- a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
+++ b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
index 1343ecd982..d6811fe14e 100644
--- a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,13 +10,20 @@ namespace MediaBrowser.Controller.LiveTv
{
public class SeriesTimerInfo
{
+ public SeriesTimerInfo()
+ {
+ Days = new List<DayOfWeek>();
+ SkipEpisodesInLibrary = true;
+ KeepUntil = KeepUntil.UntilDeleted;
+ }
+
/// <summary>
- /// Id of the recording.
+ /// Gets or sets the id of the recording.
/// </summary>
public string Id { get; set; }
/// <summary>
- /// ChannelId of the recording.
+ /// Gets or sets the channelId of the recording.
/// </summary>
public string ChannelId { get; set; }
@@ -25,24 +34,27 @@ namespace MediaBrowser.Controller.LiveTv
public string ProgramId { get; set; }
/// <summary>
- /// Name of the recording.
+ /// Gets or sets the name of the recording.
/// </summary>
public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the service name.
+ /// </summary>
public string ServiceName { get; set; }
/// <summary>
- /// Description of the recording.
+ /// Gets or sets the description of the recording.
/// </summary>
public string Overview { get; set; }
/// <summary>
- /// The start date of the recording, in UTC.
+ /// Gets or sets the start date of the recording, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the recording, in UTC.
+ /// Gets or sets the end date of the recording, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
@@ -111,12 +123,5 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The series identifier.</value>
public string SeriesId { get; set; }
-
- public SeriesTimerInfo()
- {
- Days = new List<DayOfWeek>();
- SkipEpisodesInLibrary = true;
- KeepUntil = KeepUntil.UntilDeleted;
- }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
index 1b8f41db69..92eb0be9c0 100644
--- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
index bcef4666d2..1a2e8acb31 100644
--- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -26,18 +28,17 @@ namespace MediaBrowser.Controller.LiveTv
public string[] Tags { get; set; }
/// <summary>
- /// Id of the recording.
+ /// Gets or sets the id of the recording.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the series timer identifier.
/// </summary>
- /// <value>The series timer identifier.</value>
public string SeriesTimerId { get; set; }
/// <summary>
- /// ChannelId of the recording.
+ /// Gets or sets the channelId of the recording.
/// </summary>
public string ChannelId { get; set; }
@@ -50,24 +51,24 @@ namespace MediaBrowser.Controller.LiveTv
public string ShowId { get; set; }
/// <summary>
- /// Name of the recording.
+ /// Gets or sets the name of the recording.
/// </summary>
public string Name { get; set; }
/// <summary>
- /// Description of the recording.
+ /// Gets or sets the description of the recording.
/// </summary>
public string Overview { get; set; }
public string SeriesId { get; set; }
/// <summary>
- /// The start date of the recording, in UTC.
+ /// Gets or sets the start date of the recording, in UTC.
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
- /// The end date of the recording, in UTC.
+ /// Gets or sets the end date of the recording, in UTC.
/// </summary>
public DateTime EndDate { get; set; }
@@ -113,6 +114,7 @@ namespace MediaBrowser.Controller.LiveTv
// Program properties
public int? SeasonNumber { get; set; }
+
/// <summary>
/// Gets or sets the episode number.
/// </summary>
@@ -130,7 +132,7 @@ namespace MediaBrowser.Controller.LiveTv
public bool IsSeries { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether this instance is live.
+ /// Gets a value indicating whether this instance is live.
/// </summary>
/// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
[JsonIgnore]
diff --git a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
index 2759b314f5..1c1a4417dc 100644
--- a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
+++ b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.LiveTv
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index df92eda38a..0f697bcccd 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -8,19 +8,23 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
- <VersionPrefix>10.7.0</VersionPrefix>
+ <VersionPrefix>10.8.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.6" />
+ <PackageReference Include="Diacritics" Version="2.1.20036.1" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+ <PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" />
+ <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
+ <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
</ItemGroup>
<ItemGroup>
@@ -28,22 +32,30 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>netstandard2.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
+ <PublishRepositoryUrl>true</PublishRepositoryUrl>
+ <EmbedUntrackedSources>true</EmbedUntrackedSources>
+ <IncludeSymbols>true</IncludeSymbols>
+ <SymbolPackageFormat>snupkg</SymbolPackageFormat>
+ <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release'">
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
+ <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
+ <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
new file mode 100644
index 0000000000..dd6f468dab
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -0,0 +1,204 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public class BaseEncodingJobOptions
+ {
+ public BaseEncodingJobOptions()
+ {
+ EnableAutoStreamCopy = true;
+ AllowVideoStreamCopy = true;
+ AllowAudioStreamCopy = true;
+ Context = EncodingContext.Streaming;
+ StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public Guid Id { get; set; }
+
+ public string MediaSourceId { get; set; }
+
+ public string DeviceId { get; set; }
+
+ public string Container { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio codec.
+ /// </summary>
+ /// <value>The audio codec.</value>
+ public string AudioCodec { get; set; }
+
+ public bool EnableAutoStreamCopy { get; set; }
+
+ public bool AllowVideoStreamCopy { get; set; }
+
+ public bool AllowAudioStreamCopy { get; set; }
+
+ public bool BreakOnNonKeyFrames { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio sample rate.
+ /// </summary>
+ /// <value>The audio sample rate.</value>
+ public int? AudioSampleRate { get; set; }
+
+ public int? MaxAudioBitDepth { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio bit rate.
+ /// </summary>
+ /// <value>The audio bit rate.</value>
+ public int? AudioBitRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio channels.
+ /// </summary>
+ /// <value>The audio channels.</value>
+ public int? AudioChannels { get; set; }
+
+ public int? MaxAudioChannels { get; set; }
+
+ public bool Static { get; set; }
+
+ /// <summary>
+ /// Gets or sets the profile.
+ /// </summary>
+ /// <value>The profile.</value>
+ public string Profile { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level.
+ /// </summary>
+ /// <value>The level.</value>
+ public string Level { get; set; }
+
+ /// <summary>
+ /// Gets or sets the framerate.
+ /// </summary>
+ /// <value>The framerate.</value>
+ public float? Framerate { get; set; }
+
+ public float? MaxFramerate { get; set; }
+
+ public bool CopyTimestamps { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start time ticks.
+ /// </summary>
+ /// <value>The start time ticks.</value>
+ public long? StartTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width of the max.
+ /// </summary>
+ /// <value>The width of the max.</value>
+ public int? MaxWidth { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height of the max.
+ /// </summary>
+ /// <value>The height of the max.</value>
+ public int? MaxHeight { get; set; }
+
+ /// <summary>
+ /// Gets or sets the video bit rate.
+ /// </summary>
+ /// <value>The video bit rate.</value>
+ public int? VideoBitRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the subtitle stream.
+ /// </summary>
+ /// <value>The index of the subtitle stream.</value>
+ public int? SubtitleStreamIndex { get; set; }
+
+ public SubtitleDeliveryMethod SubtitleMethod { get; set; }
+
+ public int? MaxRefFrames { get; set; }
+
+ public int? MaxVideoBitDepth { get; set; }
+
+ public bool RequireAvc { get; set; }
+
+ public bool DeInterlace { get; set; }
+
+ public bool RequireNonAnamorphic { get; set; }
+
+ public int? TranscodingMaxAudioChannels { get; set; }
+
+ public int? CpuCoreLimit { get; set; }
+
+ public string LiveStreamId { get; set; }
+
+ public bool EnableMpegtsM2TsMode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the video codec.
+ /// </summary>
+ /// <value>The video codec.</value>
+ public string VideoCodec { get; set; }
+
+ public string SubtitleCodec { get; set; }
+
+ public string TranscodeReasons { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the audio stream.
+ /// </summary>
+ /// <value>The index of the audio stream.</value>
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the video stream.
+ /// </summary>
+ /// <value>The index of the video stream.</value>
+ public int? VideoStreamIndex { get; set; }
+
+ public EncodingContext Context { get; set; }
+
+ public Dictionary<string, string> StreamOptions { get; set; }
+
+ public string GetOption(string qualifier, string name)
+ {
+ var value = GetOption(qualifier + "-" + name);
+
+ if (string.IsNullOrEmpty(value))
+ {
+ value = GetOption(name);
+ }
+
+ return value;
+ }
+
+ public string GetOption(string name)
+ {
+ if (StreamOptions.TryGetValue(name, out var value))
+ {
+ return value;
+ }
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 550916f823..141bb91c57 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -5,30 +7,24 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Configuration;
namespace MediaBrowser.Controller.MediaEncoding
{
public class EncodingHelper
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
private static readonly string[] _videoProfiles = new[]
{
@@ -43,14 +39,10 @@ namespace MediaBrowser.Controller.MediaEncoding
public EncodingHelper(
IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration)
+ ISubtitleEncoder subtitleEncoder)
{
_mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
_subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
}
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
@@ -63,22 +55,22 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
- // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+ // Since transcoding of folder rips is experimental anyway, it's not worth adding additional variables such as this.
if (state.VideoType == VideoType.VideoFile)
{
var hwType = encodingOptions.HardwareAccelerationType;
var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
- {"qsv", hwEncoder + "_qsv"},
- {hwEncoder + "_qsv", hwEncoder + "_qsv"},
- {"nvenc", hwEncoder + "_nvenc"},
- {"amf", hwEncoder + "_amf"},
- {"omx", hwEncoder + "_omx"},
- {hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m"},
- {"mediacodec", hwEncoder + "_mediacodec"},
- {"vaapi", hwEncoder + "_vaapi"},
- {"videotoolbox", hwEncoder + "_videotoolbox"}
+ { "qsv", hwEncoder + "_qsv" },
+ { hwEncoder + "_qsv", hwEncoder + "_qsv" },
+ { "nvenc", hwEncoder + "_nvenc" },
+ { "amf", hwEncoder + "_amf" },
+ { "omx", hwEncoder + "_omx" },
+ { hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m" },
+ { "mediacodec", hwEncoder + "_mediacodec" },
+ { "vaapi", hwEncoder + "_vaapi" },
+ { "videotoolbox", hwEncoder + "_videotoolbox" }
};
if (!string.IsNullOrEmpty(hwType)
@@ -109,12 +101,66 @@ namespace MediaBrowser.Controller.MediaEncoding
}
return _mediaEncoder.SupportsHwaccel("vaapi");
+ }
+
+ private bool IsCudaSupported()
+ {
+ return _mediaEncoder.SupportsHwaccel("cuda")
+ && _mediaEncoder.SupportsFilter("scale_cuda", null)
+ && _mediaEncoder.SupportsFilter("yadif_cuda", null);
+ }
+ private bool IsTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
+ {
+ var videoStream = state.VideoStream;
+ return IsColorDepth10(state)
+ && _mediaEncoder.SupportsHwaccel("opencl")
+ && options.EnableTonemapping
+ && string.Equals(videoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
+ {
+ var videoStream = state.VideoStream;
+ if (videoStream == null)
+ {
+ // Remote stream doesn't have media info, disable vpp tonemapping.
+ return false;
+ }
+
+ var codec = videoStream.Codec;
+ if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ // Limited to HEVC for now since the filter doesn't accept master data from VP9.
+ return IsColorDepth10(state)
+ && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.SupportsHwaccel("vaapi")
+ && options.EnableVppTonemapping
+ && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase);
+ }
+
+ // Hybrid VPP tonemapping for QSV with VAAPI
+ if (OperatingSystem.IsLinux() && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ // Limited to HEVC for now since the filter doesn't accept master data from VP9.
+ return IsColorDepth10(state)
+ && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.SupportsHwaccel("vaapi")
+ && _mediaEncoder.SupportsHwaccel("qsv")
+ && options.EnableVppTonemapping
+ && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase);
+ }
+
+ // Native VPP tonemapping may come to QSV in the future.
+ return false;
}
/// <summary>
/// Gets the name of the output video codec.
/// </summary>
+ /// <param name="state">Encording state.</param>
+ /// <param name="encodingOptions">Encoding options.</param>
+ /// <returns>Encoder string.</returns>
public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
{
var codec = state.OutputVideoCodec;
@@ -248,7 +294,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
- // Seeing reported failures here, not sure yet if this is related to specfying input format
+ // Seeing reported failures here, not sure yet if this is related to specifying input format
if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase))
{
return null;
@@ -260,9 +306,20 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
+ // ISO files don't have an ffmpeg format
+ if (string.Equals(container, "iso", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
return container;
}
+ /// <summary>
+ /// Gets decoder from a codec.
+ /// </summary>
+ /// <param name="codec">Codec to use.</param>
+ /// <returns>Decoder string.</returns>
public string GetDecoderFromCodec(string codec)
{
// For these need to find out the ffmpeg names
@@ -292,6 +349,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Infers the audio codec based on the url.
/// </summary>
+ /// <param name="container">Container to use.</param>
+ /// <returns>Codec string.</returns>
public string InferAudioCodec(string container)
{
var ext = "." + (container ?? string.Empty);
@@ -380,25 +439,9 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetInputPathArgument(EncodingJobInfo state)
{
- var protocol = state.InputProtocol;
var mediaPath = state.MediaPath ?? string.Empty;
- string[] inputPath;
- if (state.IsInputVideo
- && !(state.VideoType == VideoType.Iso && state.IsoMount == null))
- {
- inputPath = MediaEncoderHelpers.GetInputArgument(
- _fileSystem,
- mediaPath,
- state.IsoMount,
- state.PlayableStreamFileNames);
- }
- else
- {
- inputPath = new[] { mediaPath };
- }
-
- return _mediaEncoder.GetInputArgument(inputPath, protocol);
+ return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource);
}
/// <summary>
@@ -441,24 +484,39 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libopus";
}
+ if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
+ {
+ // flac is experimental in mp4 muxer
+ return "flac -strict -2";
+ }
+
return codec.ToLowerInvariant();
}
/// <summary>
/// Gets the input argument.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="encodingOptions">Encoding options.</param>
+ /// <returns>Input arguments.</returns>
public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions)
{
var arg = new StringBuilder();
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
+ var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+ var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
- var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+ var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
+ var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase);
+ var isWindows = OperatingSystem.IsWindows();
+ var isLinux = OperatingSystem.IsLinux();
+ var isMacOS = OperatingSystem.IsMacOS();
+ var isTonemappingSupported = IsTonemappingSupported(state, encodingOptions);
+ var isVppTonemappingSupported = IsVppTonemappingSupported(state, encodingOptions);
if (!IsCopyCodec(outputVideoCodec))
{
@@ -468,10 +526,23 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isVaapiDecoder)
{
- arg.Append("-hwaccel_output_format vaapi ")
- .Append("-vaapi_device ")
- .Append(encodingOptions.VaapiDevice)
- .Append(' ');
+ if (isTonemappingSupported && !isVppTonemappingSupported)
+ {
+ arg.Append("-init_hw_device vaapi=va:")
+ .Append(encodingOptions.VaapiDevice)
+ .Append(' ')
+ .Append("-init_hw_device opencl=ocl@va ")
+ .Append("-hwaccel_device va ")
+ .Append("-hwaccel_output_format vaapi ")
+ .Append("-filter_hw_device ocl ");
+ }
+ else
+ {
+ arg.Append("-hwaccel_output_format vaapi ")
+ .Append("-vaapi_device ")
+ .Append(encodingOptions.VaapiDevice)
+ .Append(' ');
+ }
}
else if (!isVaapiDecoder && isVaapiEncoder)
{
@@ -479,6 +550,8 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append(encodingOptions.VaapiDevice)
.Append(' ');
}
+
+ arg.Append("-autorotate 0 ");
}
if (state.IsVideoRequest
@@ -507,11 +580,47 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append("-hwaccel qsv ");
}
}
+
// While using SW decoder
- else
+ else if (isSwDecoder)
{
arg.Append("-init_hw_device qsv=hw -filter_hw_device hw ");
}
+
+ // Hybrid VPP tonemapping with VAAPI
+ else if (isVaapiDecoder && isVppTonemappingSupported)
+ {
+ arg.Append("-init_hw_device vaapi=va:")
+ .Append(encodingOptions.VaapiDevice)
+ .Append(' ')
+ .Append("-init_hw_device qsv@va ")
+ .Append("-hwaccel_output_format vaapi ");
+ }
+
+ arg.Append("-autorotate 0 ");
+ }
+ }
+
+ if (state.IsVideoRequest
+ && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
+ && isNvdecDecoder)
+ {
+ // Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562
+ arg.Append("-hwaccel_output_format cuda -extra_hw_frames 3 -autorotate 0 ");
+ }
+
+ if (state.IsVideoRequest
+ && ((string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
+ && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder))
+ || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)
+ && (isD3d11vaDecoder || isSwDecoder))))
+ {
+ if (isTonemappingSupported)
+ {
+ arg.Append("-init_hw_device opencl=ocl:")
+ .Append(encodingOptions.OpenclDevice)
+ .Append(' ')
+ .Append("-filter_hw_device ocl ");
}
}
@@ -551,7 +660,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
- public bool IsH264(MediaStream stream)
+ public static bool IsH264(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
@@ -559,7 +668,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
}
- public bool IsH265(MediaStream stream)
+ public static bool IsH265(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
@@ -567,10 +676,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
}
- // TODO This is auto inserted into the mpegts mux so it might not be needed
- // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
- public string GetBitStreamArgs(MediaStream stream)
+ public static bool IsAAC(MediaStream stream)
+ {
+ var codec = stream.Codec ?? string.Empty;
+
+ return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ public static string GetBitStreamArgs(MediaStream stream)
{
+ // TODO This is auto inserted into the mpegts mux so it might not be needed.
+ // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
if (IsH264(stream))
{
return "-bsf:v h264_mp4toannexb";
@@ -579,12 +695,44 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return "-bsf:v hevc_mp4toannexb";
}
+ else if (IsAAC(stream))
+ {
+ // Convert adts header(mpegts) to asc header(mp4).
+ return "-bsf:a aac_adtstoasc";
+ }
else
{
return null;
}
}
+ public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
+ {
+ var bitStreamArgs = string.Empty;
+ var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
+
+ // Apply aac_adtstoasc bitstream filter when media source is in mpegts.
+ if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
+ {
+ bitStreamArgs = GetBitStreamArgs(state.AudioStream);
+ bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
+ }
+
+ return bitStreamArgs;
+ }
+
+ public static string GetSegmentFileExtension(string segmentContainer)
+ {
+ if (!string.IsNullOrWhiteSpace(segmentContainer))
+ {
+ return "." + segmentContainer;
+ }
+
+ return ".ts";
+ }
+
public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
var bitrate = state.OutputVideoBitrate;
@@ -632,16 +780,30 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- public string NormalizeTranscodingLevel(string videoCodec, string level)
+ public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
- // Clients may direct play higher than level 41, but there's no reason to transcode higher
- if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel)
- && requestLevel > 41
- && (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)))
+ if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel))
{
- return "41";
+ if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
+ {
+ // Transcode to level 5.0 and lower for maximum compatibility.
+ // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
+ // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
+ // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
+ if (requestLevel >= 150)
+ {
+ return "150";
+ }
+ }
+ else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ // Clients may direct play higher than level 41, but there's no reason to transcode higher.
+ if (requestLevel >= 41)
+ {
+ return "41";
+ }
+ }
}
return level;
@@ -744,13 +906,130 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
+ public string GetHlsVideoKeyFrameArguments(
+ EncodingJobInfo state,
+ string codec,
+ int segmentLength,
+ bool isEventPlaylist,
+ int? startNumber)
+ {
+ var args = string.Empty;
+ var gopArg = string.Empty;
+ var keyFrameArg = string.Empty;
+ if (isEventPlaylist)
+ {
+ keyFrameArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"",
+ segmentLength);
+ }
+ else if (startNumber.HasValue)
+ {
+ keyFrameArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
+ startNumber.Value * segmentLength,
+ segmentLength);
+ }
+
+ var framerate = state.VideoStream?.RealFrameRate;
+ if (framerate.HasValue)
+ {
+ // This is to make sure keyframe interval is limited to our segment,
+ // as forcing keyframes is not enough.
+ // Example: we encoded half of desired length, then codec detected
+ // scene cut and inserted a keyframe; next forced keyframe would
+ // be created outside of segment, which breaks seeking.
+ // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe.
+ gopArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -g:v:0 {0} -keyint_min:v:0 {0} -sc_threshold:v:0 0",
+ Math.Ceiling(segmentLength * framerate.Value));
+ }
+
+ // Unable to force key frames using these encoders, set key frames by GOP.
+ if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ args += gopArg;
+ }
+ else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ args += " " + keyFrameArg;
+ }
+ else
+ {
+ args += " " + keyFrameArg + gopArg;
+ }
+
+ return args;
+ }
+
/// <summary>
/// Gets the video bitrate to specify on the command line.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="videoEncoder">Video encoder to use.</param>
+ /// <param name="encodingOptions">Encoding options.</param>
+ /// <param name="defaultPreset">Default present to use for encoding.</param>
+ /// <returns>Video bitrate.</returns>
public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset)
{
var param = string.Empty;
+ if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -pix_fmt yuv420p";
+ }
+
+ if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ var videoStream = state.VideoStream;
+ var isColorDepth10 = IsColorDepth10(state);
+ var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
+ var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
+
+ if (!isNvdecDecoder)
+ {
+ if (isColorDepth10
+ && _mediaEncoder.SupportsHwaccel("opencl")
+ && encodingOptions.EnableTonemapping
+ && !string.IsNullOrEmpty(videoStream.VideoRange)
+ && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -pix_fmt nv12";
+ }
+ else
+ {
+ param += " -pix_fmt yuv420p";
+ }
+ }
+ }
+
+ if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -pix_fmt nv21";
+ }
+
var isVc1 = state.VideoStream != null &&
string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
@@ -759,11 +1038,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset))
{
- param += "-preset " + encodingOptions.EncoderPreset;
+ param += " -preset " + encodingOptions.EncoderPreset;
}
else
{
- param += "-preset " + defaultPreset;
+ param += " -preset " + defaultPreset;
}
int encodeCrf = encodingOptions.H264Crf;
@@ -787,38 +1066,39 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -crf " + defaultCrf;
}
}
- else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) // h264 (h264_qsv)
+ else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
{
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase))
{
- param += "-preset " + encodingOptions.EncoderPreset;
+ param += " -preset " + encodingOptions.EncoderPreset;
}
else
{
- param += "-preset 7";
+ param += " -preset 7";
}
param += " -look_ahead 0";
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
- || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
{
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
- param += "-preset slow"; // lossless is only supported on maxwell and newer(2014+)
+ param += " -preset slow"; // lossless is only supported on maxwell and newer(2014+)
break;
case "slow":
case "slower":
- param += "-preset slow";
+ param += " -preset slow";
break;
case "medium":
- param += "-preset medium";
+ param += " -preset medium";
break;
case "fast":
@@ -826,27 +1106,27 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast":
case "superfast":
case "ultrafast":
- param += "-preset fast";
+ param += " -preset fast";
break;
default:
- param += "-preset default";
+ param += " -preset default";
break;
}
}
- else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
{
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
case "slow":
case "slower":
- param += "-quality quality";
+ param += " -quality quality";
break;
case "medium":
- param += "-quality balanced";
+ param += " -quality balanced";
break;
case "fast":
@@ -854,13 +1134,31 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast":
case "superfast":
case "ultrafast":
- param += "-quality speed";
+ param += " -quality speed";
break;
default:
- param += "-quality speed";
+ param += " -quality speed";
break;
}
+
+ var videoStream = state.VideoStream;
+ var isColorDepth10 = IsColorDepth10(state);
+
+ if (isColorDepth10
+ && _mediaEncoder.SupportsHwaccel("opencl")
+ && encodingOptions.EnableTonemapping
+ && !string.IsNullOrEmpty(videoStream.VideoRange)
+ && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ // Enhance workload when tone mapping with AMF on some APUs
+ param += " -preanalysis true";
+ }
+
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -header_insertion_mode gop -gops_per_idr 1";
+ }
}
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{
@@ -882,7 +1180,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,
@@ -890,15 +1190,15 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
{
- param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
+ param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
}
else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv
{
- param += "-qmin 2";
+ param += " -qmin 2";
}
else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
{
- param += "-mbd 2";
+ param += " -mbd 2";
}
param += GetVideoBitrateParam(state, videoEncoder);
@@ -910,25 +1210,88 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var targetVideoCodec = state.ActualOutputVideoCodec;
+ if (string.Equals(targetVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(targetVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ targetVideoCodec = "hevc";
+ }
+
+ var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
+ profile = Regex.Replace(profile, @"\s+", string.Empty);
- var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault();
+ // We only transcode to HEVC 8-bit for now, force Main Profile.
+ if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)
+ || profile.Contains("mainstill", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "main";
+ }
+
+ // Extended Profile is not supported by any known h264 encoders, force Main Profile.
+ if (profile.Contains("extended", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "main";
+ }
- // vaapi does not support Baseline profile, force Constrained Baseline in this case,
- // which is compatible (and ugly)
+ // Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
+ if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("high10", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "high";
+ }
+
+ // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
+ // which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- && profile != null
- && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
+ && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "constrained_baseline";
+ }
+
+ // libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case.
+ if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "baseline";
+ }
+
+ // libx264, h264_qsv, h264_nvenc and h264_vaapi does not support Constrained High profile, force High in this case.
+ if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ && profile.Contains("high", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "high";
+ }
+
+ if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
{
profile = "constrained_baseline";
}
+ if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "constrained_high";
+ }
+
+ // Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile.
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ && profile.Contains("main10", StringComparison.OrdinalIgnoreCase))
+ {
+ profile = "main";
+ }
+
if (!string.IsNullOrEmpty(profile))
{
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
// not supported by h264_omx
- param += " -profile:v " + profile;
+ param += " -profile:v:0 " + profile;
}
}
@@ -936,55 +1299,35 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(level))
{
- level = NormalizeTranscodingLevel(state.OutputVideoCodec, level);
+ level = NormalizeTranscodingLevel(state, level);
- // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
- // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
+ // libx264, QSV, AMF, VAAPI can adjust the given level to match the output.
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
- {
- switch (level)
- {
- case "30":
- param += " -level 3.0";
- break;
- case "31":
- param += " -level 3.1";
- break;
- case "32":
- param += " -level 3.2";
- break;
- case "40":
- param += " -level 4.0";
- break;
- case "41":
- param += " -level 4.1";
- break;
- case "42":
- param += " -level 4.2";
- break;
- case "50":
- param += " -level 5.0";
- break;
- case "51":
- param += " -level 5.1";
- break;
- case "52":
- param += " -level 5.2";
- break;
- default:
- param += " -level " + level;
- break;
+ || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -level " + level;
+ }
+ else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ // hevc_qsv use -level 51 instead of -level 153.
+ if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
+ {
+ param += " -level " + (hevcLevel / 3);
}
}
+ else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -level " + level;
+ }
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
{
- // nvenc doesn't decode with param -level set ?!
- // TODO:
+ // level option may cause NVENC to fail.
+ // NVENC cannot adjust the given level, just throw an error.
}
- else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
+ || !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
param += " -level " + level;
}
@@ -997,20 +1340,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
- // todo
- }
-
- if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
- {
- param = "-pix_fmt yuv420p " + param;
- }
-
- if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
- {
- param = "-pix_fmt nv21 " + param;
+ // libx265 only accept level option in -x265-params.
+ // level option may cause libx265 to fail.
+ // libx265 cannot adjust the given level, just throw an error.
+ // TODO: set fine tuned params.
+ param += " -x265-params:0 no-info=1";
}
return param;
@@ -1073,7 +1407,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedProfile = requestedProfiles[0];
// strip spaces because they may be stripped out on the query string as well
- if (!string.IsNullOrEmpty(videoStream.Profile) && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", ""), StringComparer.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(videoStream.Profile)
+ && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", string.Empty, StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
{
var currentScore = GetVideoProfileScore(videoStream.Profile);
var requestedScore = GetVideoProfileScore(requestedProfile);
@@ -1289,7 +1624,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
- return .5;
+ return .6;
}
return 1;
@@ -1323,27 +1658,55 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
{
- if (request.AudioBitRate.HasValue)
- {
- // Don't encode any higher than this
- return Math.Min(384000, request.AudioBitRate.Value);
- }
-
- return null;
+ return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
}
- public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
+ public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
{
- if (audioBitRate.HasValue)
+ if (audioStream == null)
+ {
+ return null;
+ }
+
+ if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
{
- // Don't encode any higher than this
return Math.Min(384000, audioBitRate.Value);
}
- return null;
+ if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
+ {
+ if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ if ((audioStream.Channels ?? 0) >= 6)
+ {
+ return Math.Min(640000, audioBitRate.Value);
+ }
+
+ return Math.Min(384000, audioBitRate.Value);
+ }
+
+ if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ if ((audioStream.Channels ?? 0) >= 6)
+ {
+ return Math.Min(3584000, audioBitRate.Value);
+ }
+
+ return Math.Min(1536000, audioBitRate.Value);
+ }
+ }
+
+ // Empty bitrate area is not allow on iOS
+ // Default audio bitrate to 128K if it is not being requested
+ // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
+ return 128000;
}
- public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)
+ public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions)
{
var channels = state.OutputAudioChannels;
@@ -1374,7 +1737,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (filters.Count > 0)
{
- return "-af \"" + string.Join(",", filters) + "\"";
+ return " -af \"" + string.Join(',', filters) + "\"";
}
return string.Empty;
@@ -1389,6 +1752,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.Nullable{System.Int32}.</returns>
public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec)
{
+ if (audioStream == null)
+ {
+ return null;
+ }
+
var request = state.BaseRequest;
var inputChannels = audioStream?.Channels;
@@ -1400,7 +1768,6 @@ namespace MediaBrowser.Controller.MediaEncoding
var codec = outputAudioCodec ?? string.Empty;
-
int? transcoderChannelLimit;
if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
{
@@ -1412,6 +1779,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// libmp3lame currently only supports two channel output
transcoderChannelLimit = 2;
}
+ else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ // aac is able to handle 8ch(7.1 layout)
+ transcoderChannelLimit = 8;
+ }
else
{
// If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
@@ -1440,6 +1812,16 @@ namespace MediaBrowser.Controller.MediaEncoding
: transcoderChannelLimit.Value;
}
+ // Avoid transcoding to audio channels other than 1ch, 2ch, 6ch (5.1 layout) and 8ch (7.1 layout).
+ // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices
+ if (isTranscodingAudio
+ && state.TranscodingType != TranscodingJobType.Progressive
+ && resultChannels.HasValue
+ && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7))
+ {
+ resultChannels = 2;
+ }
+
return resultChannels;
}
@@ -1599,8 +1981,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets the graphical subtitle param.
+ /// Gets the graphical subtitle parameter.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="outputVideoCodec">Video codec to use.</param>
+ /// <returns>Graphical subtitle parameter.</returns>
public string GetGraphicalSubtitleParam(
EncodingJobInfo state,
EncodingOptions options,
@@ -1611,70 +1997,30 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputSizeParam = ReadOnlySpan<char>.Empty;
var request = state.BaseRequest;
- // Add resolution params, if specified
- if (request.Width.HasValue
- || request.Height.HasValue
- || request.MaxHeight.HasValue
- || request.MaxWidth.HasValue)
- {
- outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
-
- // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
- var index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- outputSizeParam = outputSizeParam.Slice(index);
- }
- else
- {
- // vpp_qsv
- index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- outputSizeParam = outputSizeParam.Slice(index);
- }
- else
- {
- // hwdownload,format=p010le (hardware decode + software encode for vaapi)
- index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- outputSizeParam = outputSizeParam.Slice(index);
- }
- else
- {
- // format=nv12|vaapi,hwupload,scale_vaapi
- index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- outputSizeParam = outputSizeParam.Slice(index);
- }
- else
- {
- // yadif,scale=expr
- index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- outputSizeParam = outputSizeParam.Slice(index);
- }
- else
- {
- // scale=expr
- index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- outputSizeParam = outputSizeParam.Slice(index);
- }
- }
- }
- }
- }
- }
- }
+ outputSizeParam = GetOutputSizeParamInternal(state, options, outputVideoCodec);
var videoSizeParam = string.Empty;
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ var isLinux = OperatingSystem.IsLinux();
+
+ var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isQsvH264Encoder = outputVideoCodec.Contains("h264_qsv", StringComparison.OrdinalIgnoreCase);
+ var isQsvHevcEncoder = outputVideoCodec.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
+ var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
+ var isNvencEncoder = outputVideoCodec.Contains("nvenc", 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 isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
+
+ // Tonemapping and burn-in graphical subtitles requires overlay_vaapi.
+ // But it's still in ffmpeg mailing list. Disable it for now.
+ if (isTonemappingSupportedOnVaapi && isTonemappingSupported && !isVppTonemappingSupported)
+ {
+ return GetOutputSizeParam(state, options, outputVideoCodec);
+ }
// Setup subtitle scaling
if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
@@ -1694,10 +2040,15 @@ namespace MediaBrowser.Controller.MediaEncoding
height.Value);
}
- // For QSV, feed it into hardware encoder now
- if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(videoSizeParam)
+ && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported))
{
- videoSizeParam += ",hwupload=extra_hw_frames=64";
+ // For QSV, feed it into hardware encoder now
+ if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)))
+ {
+ videoSizeParam += ",hwupload=extra_hw_frames=64";
+ }
}
}
@@ -1716,28 +2067,35 @@ namespace MediaBrowser.Controller.MediaEncoding
: " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable
- if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
{
/*
[base]: HW scaling video to OutputSize
[sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay
*/
- retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
+ retStr = !outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
- && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+ && (string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "libx265", StringComparison.OrdinalIgnoreCase)))
{
/*
[base]: SW scaling video to OutputSize
[sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay
*/
- retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
+ retStr = !outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
}
- else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
{
/*
QSV in FFMpeg can now setup hardware overlay for transcodes.
@@ -1745,12 +2103,22 @@ namespace MediaBrowser.Controller.MediaEncoding
with fixed frame size.
Currently only supports linux.
*/
- if (isLinux)
+ if (isTonemappingSupportedOnQsv && isVppTonemappingSupported)
{
- retStr = !outputSizeParam.IsEmpty ?
- " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" :
- " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
+ retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload,format=nv12[base];[base][sub]overlay\"";
}
+ else if (isLinux)
+ {
+ retStr = !outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
+ }
+ }
+ else if (isNvdecDecoder && isNvencEncoder)
+ {
+ retStr = !outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=nv12|yuv420p,hwupload_cuda\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=nv12|yuv420p,hwupload_cuda\"";
}
return string.Format(
@@ -1763,7 +2131,7 @@ namespace MediaBrowser.Controller.MediaEncoding
videoSizeParam);
}
- private (int? width, int? height) GetFixedOutputSize(
+ public static (int? width, int? height) GetFixedOutputSize(
int? videoWidth,
int? videoHeight,
int? requestedWidth,
@@ -1781,8 +2149,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;
@@ -1801,7 +2169,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return (Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight));
}
- public List<string> GetScalingFilters(EncodingJobInfo state,
+ public List<string> GetScalingFilters(
+ EncodingJobInfo state,
+ EncodingOptions options,
int? videoWidth,
int? videoHeight,
Video3DFormat? threedFormat,
@@ -1822,7 +2192,9 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxHeight);
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
&& width.HasValue
&& height.HasValue)
{
@@ -1831,12 +2203,36 @@ namespace MediaBrowser.Controller.MediaEncoding
// output dimensions. Output dimensions are guaranteed to be even.
var outputWidth = width.Value;
var outputHeight = height.Value;
- var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
+ var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase);
var isDeintEnabled = state.DeInterlace("h264", true)
|| state.DeInterlace("avc", true)
|| state.DeInterlace("h265", true)
|| state.DeInterlace("hevc", true);
+ var isVaapiDecoder = videoDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
+ var isVaapiH264Encoder = videoEncoder.Contains("h264_vaapi", StringComparison.OrdinalIgnoreCase);
+ var isVaapiHevcEncoder = videoEncoder.Contains("hevc_vaapi", StringComparison.OrdinalIgnoreCase);
+ var isQsvH264Encoder = videoEncoder.Contains("h264_qsv", StringComparison.OrdinalIgnoreCase);
+ 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 isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
+ var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported))
+ || (isTonemappingSupportedOnQsv && isVppTonemappingSupported);
+
+ var outputPixFmt = "format=nv12";
+ if (isP010PixFmtRequired)
+ {
+ outputPixFmt = "format=p010";
+ }
+
+ if (isTonemappingSupportedOnQsv && isVppTonemappingSupported)
+ {
+ qsv_or_vaapi = false;
+ }
+
if (!videoWidth.HasValue
|| outputWidth != videoWidth.Value
|| !videoHeight.HasValue
@@ -1847,22 +2243,69 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add(
string.Format(
CultureInfo.InvariantCulture,
- "{0}=w={1}:h={2}:format=nv12{3}",
+ "{0}=w={1}:h={2}{3}{4}",
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
outputWidth,
outputHeight,
+ ":" + outputPixFmt,
(qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
}
- else
+
+ // Assert 10-bit is P010 so as we can avoid the extra scaler to get a bit more fps on high res HDR videos.
+ else if (!isP010PixFmtRequired)
{
filters.Add(
string.Format(
CultureInfo.InvariantCulture,
- "{0}=format=nv12{1}",
+ "{0}={1}{2}",
qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
+ outputPixFmt,
(qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
}
}
+ else if ((videoDecoder ?? string.Empty).Contains("cuda", StringComparison.OrdinalIgnoreCase)
+ && width.HasValue
+ && height.HasValue)
+ {
+ var outputWidth = width.Value;
+ var outputHeight = height.Value;
+
+ var isTonemappingSupported = IsTonemappingSupported(state, options);
+ var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase);
+ var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilter("scale_cuda", "Output format (default \"same\")");
+
+ var outputPixFmt = string.Empty;
+ if (isCudaFormatConversionSupported)
+ {
+ outputPixFmt = "format=nv12";
+ if (isTonemappingSupported && isTonemappingSupportedOnNvenc)
+ {
+ outputPixFmt = "format=p010";
+ }
+ }
+
+ if (!videoWidth.HasValue
+ || outputWidth != videoWidth.Value
+ || !videoHeight.HasValue
+ || outputHeight != videoHeight.Value)
+ {
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "scale_cuda=w={0}:h={1}{2}",
+ outputWidth,
+ outputHeight,
+ isCudaFormatConversionSupported ? (":" + outputPixFmt) : string.Empty));
+ }
+ else if (isCudaFormatConversionSupported)
+ {
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "scale_cuda={0}",
+ outputPixFmt));
+ }
+ }
else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
&& width.HasValue
&& height.HasValue)
@@ -2062,13 +2505,34 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// If we're going to put a fixed size on the command line, this will calculate it.
+ /// Gets the output size parameter.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="outputVideoCodec">Video codec to use.</param>
+ /// <returns>The output size parameter.</returns>
public string GetOutputSizeParam(
EncodingJobInfo state,
EncodingOptions options,
string outputVideoCodec)
{
+ string filters = GetOutputSizeParamInternal(state, options, outputVideoCodec);
+ return string.IsNullOrEmpty(filters) ? string.Empty : " -vf \"" + filters + "\"";
+ }
+
+ /// <summary>
+ /// Gets the output size parameter.
+ /// If we're going to put a fixed size on the command line, this will calculate it.
+ /// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="outputVideoCodec">Video codec to use.</param>
+ /// <returns>The output size parameter.</returns>
+ public string GetOutputSizeParamInternal(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string outputVideoCodec)
+ {
// http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
var request = state.BaseRequest;
@@ -2080,34 +2544,181 @@ namespace MediaBrowser.Controller.MediaEncoding
var inputHeight = videoStream?.Height;
var threeDFormat = state.MediaSource.Video3DFormat;
+ var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+ var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+ var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
- var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+ var isQsvHevcEncoder = outputVideoCodec.IndexOf("hevc_qsv", StringComparison.OrdinalIgnoreCase) != -1;
+ var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
+ var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
+ var isCuvidH264Decoder = videoDecoder.Contains("h264_cuvid", StringComparison.OrdinalIgnoreCase);
+ var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase);
var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
-
+ var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1;
+ var isLinux = OperatingSystem.IsLinux();
+ var isColorDepth10 = IsColorDepth10(state);
+ var isTonemappingSupported = IsTonemappingSupported(state, options);
+ var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
+ var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder);
+ var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder);
+ 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 hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
- // When the input may or may not be hardware VAAPI decodable
- if (isVaapiH264Encoder)
+ // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices
+ var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.AverageFrameRate ?? 60) <= 30;
+
+ var isScalingInAdvance = false;
+ var isCudaDeintInAdvance = false;
+ var isHwuploadCudaRequired = false;
+ var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
+
+ // Add OpenCL tonemapping filter for NVENC/AMF/VAAPI.
+ if (isTonemappingSupportedOnNvenc || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported))
+ {
+ // Currently only with the use of NVENC decoder can we get a decent performance.
+ // Currently only the HEVC/H265 format is supported with NVDEC decoder.
+ // NVIDIA Pascal and Turing or higher are recommended.
+ // AMD Polaris and Vega or higher are recommended.
+ // Intel Kaby Lake or newer is required.
+ if (isTonemappingSupported)
+ {
+ var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
+
+ if (options.TonemappingParam != 0)
+ {
+ parameters += ":param={4}";
+ }
+
+ if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+ {
+ parameters += ":range={5}";
+ }
+
+ if (isSwDecoder || isD3d11vaDecoder)
+ {
+ isScalingInAdvance = true;
+ // Add zscale filter before tone mapping filter for performance.
+ var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
+ if (width.HasValue && height.HasValue)
+ {
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "zscale=s={0}x{1}",
+ width.Value,
+ height.Value));
+ }
+
+ // Convert to hardware pixel format p010 when using SW decoder.
+ filters.Add("format=p010");
+ }
+
+ if ((isDeinterlaceH264 || isDeinterlaceHevc) && isNvdecDecoder)
+ {
+ isCudaDeintInAdvance = true;
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "yadif_cuda={0}:-1:0",
+ doubleRateDeinterlace ? "1" : "0"));
+ }
+
+ if (isVaapiDecoder || isNvdecDecoder)
+ {
+ isScalingInAdvance = true;
+ filters.AddRange(
+ GetScalingFilters(
+ state,
+ options,
+ inputWidth,
+ inputHeight,
+ threeDFormat,
+ videoDecoder,
+ outputVideoCodec,
+ request.Width,
+ request.Height,
+ request.MaxWidth,
+ request.MaxHeight));
+ }
+
+ // hwmap the HDR data to opencl device by cl-va p010 interop.
+ if (isVaapiDecoder)
+ {
+ filters.Add("hwmap");
+ }
+
+ // convert cuda device data to p010 host data.
+ if (isNvdecDecoder)
+ {
+ filters.Add("hwdownload,format=p010");
+ }
+
+ if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder)
+ {
+ // Upload the HDR10 or HLG data to the OpenCL device,
+ // use tonemap_opencl filter for tone mapping,
+ // and then download the SDR data to memory.
+ filters.Add("hwupload");
+ }
+
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ parameters,
+ options.TonemappingAlgorithm,
+ options.TonemappingDesat,
+ options.TonemappingThreshold,
+ options.TonemappingPeak,
+ options.TonemappingParam,
+ options.TonemappingRange));
+
+ if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder)
+ {
+ filters.Add("hwdownload");
+ filters.Add("format=nv12");
+ }
+
+ if (isNvdecDecoder && isNvencEncoder)
+ {
+ isHwuploadCudaRequired = true;
+ }
+
+ if (isVaapiDecoder)
+ {
+ // Reverse the data route from opencl to vaapi.
+ filters.Add("hwmap=derive_device=vaapi:reverse=1");
+ }
+ }
+ }
+
+ // When the input may or may not be hardware VAAPI decodable.
+ if ((isVaapiH264Encoder || isVaapiHevcEncoder)
+ && !(isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported)))
{
filters.Add("format=nv12|vaapi");
filters.Add("hwupload");
}
- // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
- else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
+ // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context.
+ else if (isLinux && hasGraphicalSubs && (isQsvH264Encoder || isQsvHevcEncoder)
+ && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported))
{
filters.Add("hwupload=extra_hw_frames=64");
}
- // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
- else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
+ // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first.
+ else if ((IsVaapiSupported(state) && isVaapiDecoder) && (isLibX264Encoder || isLibX265Encoder)
+ && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported))
{
- var codec = videoStream.Codec.ToLowerInvariant();
- var isColorDepth10 = IsColorDepth10(state);
+ var codec = videoStream.Codec;
// Assert 10-bit hardware VAAPI decodable
if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -2131,59 +2742,153 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- // Add hardware deinterlace filter before scaling filter
- if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
+ // Add hardware deinterlace filter before scaling filter.
+ if (isDeinterlaceH264 || isDeinterlaceHevc)
{
- if (isVaapiH264Encoder)
+ if (isVaapiEncoder
+ || (isTonemappingSupportedOnQsv && isVppTonemappingSupported))
{
- filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi"));
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "deinterlace_vaapi=rate={0}",
+ doubleRateDeinterlace ? "field" : "frame"));
+ }
+ else if (isNvdecDecoder && !isCudaDeintInAdvance)
+ {
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "yadif_cuda={0}:-1:0",
+ doubleRateDeinterlace ? "1" : "0"));
}
}
- // Add software deinterlace filter before scaling filter
- if (state.DeInterlace("h264", true)
- || state.DeInterlace("avc", true)
- || state.DeInterlace("h265", true)
- || state.DeInterlace("hevc", true))
+ // Add software deinterlace filter before scaling filter.
+ if ((isDeinterlaceH264 || isDeinterlaceHevc)
+ && !isVaapiH264Encoder
+ && !isVaapiHevcEncoder
+ && !isQsvH264Encoder
+ && !isQsvHevcEncoder
+ && !isNvdecDecoder
+ && !isCuvidH264Decoder)
{
- string deintParam;
- var inputFramerate = videoStream?.RealFrameRate;
-
- // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle
- if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30)
+ if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase))
{
- deintParam = "yadif=1:-1:0";
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "bwdif={0}:-1:0",
+ doubleRateDeinterlace ? "1" : "0"));
}
else
{
- deintParam = "yadif=0:-1:0";
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "yadif={0}:-1:0",
+ doubleRateDeinterlace ? "1" : "0"));
}
+ }
+
+ // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
+ if (!isScalingInAdvance)
+ {
+ filters.AddRange(
+ GetScalingFilters(
+ state,
+ options,
+ inputWidth,
+ inputHeight,
+ threeDFormat,
+ videoDecoder,
+ outputVideoCodec,
+ request.Width,
+ request.Height,
+ request.MaxWidth,
+ request.MaxHeight));
+ }
- if (!string.IsNullOrEmpty(deintParam))
+ // Add VPP tonemapping filter for VAAPI.
+ // Full hardware based video post processing, faster than OpenCL but lacks fine tuning options.
+ if ((isTonemappingSupportedOnVaapi || isTonemappingSupportedOnQsv)
+ && isVppTonemappingSupported)
+ {
+ filters.Add("tonemap_vaapi=format=nv12:transfer=bt709:matrix=bt709:primaries=bt709");
+ }
+
+ // Another case is when using Nvenc decoder.
+ if (isNvdecDecoder && !isTonemappingSupported)
+ {
+ var codec = videoStream.Codec;
+ var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilter("scale_cuda", "Output format (default \"same\")");
+
+ // Assert 10-bit hardware decodable
+ if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)))
{
- if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
+ if (isCudaFormatConversionSupported)
+ {
+ if (isLibX264Encoder || isLibX265Encoder || hasSubs)
+ {
+ if (isNvencEncoder)
+ {
+ isHwuploadCudaRequired = true;
+ }
+
+ filters.Add("hwdownload");
+ filters.Add("format=nv12");
+ }
+ }
+ else
{
- filters.Add(deintParam);
+ // Download data from GPU to CPU as p010 format.
+ filters.Add("hwdownload");
+ filters.Add("format=p010");
+
+ // Cuda lacks of a pixel format converter.
+ if (isNvencEncoder)
+ {
+ isHwuploadCudaRequired = true;
+ filters.Add("format=yuv420p");
+ }
}
}
- }
- // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
- filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
+ // Assert 8-bit hardware decodable
+ else if (!isColorDepth10 && (isLibX264Encoder || isLibX265Encoder || hasSubs))
+ {
+ if (isNvencEncoder)
+ {
+ isHwuploadCudaRequired = true;
+ }
+
+ filters.Add("hwdownload");
+ filters.Add("format=nv12");
+ }
+ }
// Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
- if (isVaapiH264Encoder)
+ if (isVaapiH264Encoder
+ || isVaapiHevcEncoder
+ || (isTonemappingSupportedOnQsv && isVppTonemappingSupported))
{
if (hasTextSubs)
{
+ // Convert hw context from ocl to va.
+ // For tonemapping and text subs burn-in.
+ if (isTonemappingSupportedOnVaapi && isTonemappingSupported && !isVppTonemappingSupported)
+ {
+ filters.Add("scale_vaapi");
+ }
+
// Test passed on Intel and AMD gfx
filters.Add("hwmap=mode=read+write");
filters.Add("format=nv12");
}
}
- var output = string.Empty;
-
if (hasTextSubs)
{
var subParam = GetTextSubtitleParam(state);
@@ -2192,18 +2897,40 @@ namespace MediaBrowser.Controller.MediaEncoding
// Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
// Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
- if (isVaapiH264Encoder)
+ if (isVaapiH264Encoder || isVaapiHevcEncoder)
{
filters.Add("hwmap");
}
+
+ if (isTonemappingSupportedOnQsv && isVppTonemappingSupported)
+ {
+ filters.Add("hwmap,format=vaapi");
+ }
+
+ if (isNvdecDecoder && isNvencEncoder)
+ {
+ isHwuploadCudaRequired = true;
+ }
+ }
+
+ // Interop the VAAPI data to QSV for hybrid tonemapping
+ if (isTonemappingSupportedOnQsv && isVppTonemappingSupported && !hasGraphicalSubs)
+ {
+ filters.Add("hwmap=derive_device=qsv,scale_qsv");
}
+ if (isHwuploadCudaRequired && !hasGraphicalSubs)
+ {
+ filters.Add("hwupload_cuda");
+ }
+
+ var output = string.Empty;
if (filters.Count > 0)
{
output += string.Format(
CultureInfo.InvariantCulture,
- " -vf \"{0}\"",
- string.Join(",", filters));
+ "{0}",
+ string.Join(',', filters));
}
return output;
@@ -2212,7 +2939,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the number of threads.
/// </summary>
- public int GetNumberOfThreads(EncodingJobInfo state, EncodingOptions encodingOptions, string outputVideoCodec)
+ /// <param name="state">Encoding state.</param>
+ /// <param name="encodingOptions">Encoding options.</param>
+ /// <param name="outputVideoCodec">Video codec to use.</param>
+ /// <returns>Number of threads.</returns>
+#nullable enable
+ public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec)
{
if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
{
@@ -2222,17 +2954,22 @@ namespace MediaBrowser.Controller.MediaEncoding
return Math.Max(Environment.ProcessorCount - 1, 1);
}
- var threads = state.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
+ var threads = state?.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
// Automatic
- if (threads <= 0 || threads >= Environment.ProcessorCount)
+ if (threads <= 0)
{
return 0;
}
+ else if (threads >= Environment.ProcessorCount)
+ {
+ return Environment.ProcessorCount;
+ }
return threads;
}
+#nullable disable
public void TryStreamCopy(EncodingJobInfo state)
{
if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream))
@@ -2267,18 +3004,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- public string GetProbeSizeArgument(int numInputFiles)
- => numInputFiles > 1 ? "-probesize " + _configuration.GetFFmpegProbeSize() : string.Empty;
-
- public string GetAnalyzeDurationArgument(int numInputFiles)
- => numInputFiles > 1 ? "-analyzeduration " + _configuration.GetFFmpegAnalyzeDuration() : string.Empty;
-
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions)
{
var inputModifier = string.Empty;
-
- var numInputFiles = state.PlayableStreamFileNames.Length > 0 ? state.PlayableStreamFileNames.Length : 1;
- var probeSizeArgument = GetProbeSizeArgument(numInputFiles);
+ var probeSizeArgument = string.Empty;
string analyzeDurationArgument;
if (state.MediaSource.AnalyzeDurationMs.HasValue)
@@ -2287,7 +3016,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else
{
- analyzeDurationArgument = GetAnalyzeDurationArgument(numInputFiles);
+ analyzeDurationArgument = string.Empty;
}
if (!string.IsNullOrEmpty(probeSizeArgument))
@@ -2397,6 +3126,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.DeInterlace("h264", true))
{
inputModifier += " -deint 1";
+
+ if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.AverageFrameRate ?? 60) > 30)
+ {
+ inputModifier += " -drop_second_field 1";
+ }
}
}
}
@@ -2433,9 +3167,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return inputModifier;
}
-
public void AttachMediaSourceInfo(
EncodingJobInfo state,
+ EncodingOptions encodingOptions,
MediaSourceInfo mediaSource,
string requestedUrl)
{
@@ -2466,32 +3200,6 @@ namespace MediaBrowser.Controller.MediaEncoding
state.IsoType = mediaSource.IsoType;
- if (mediaSource.VideoType.HasValue)
- {
- state.VideoType = mediaSource.VideoType.Value;
-
- if (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd)
- {
- state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, mediaSource.VideoType.Value).Select(Path.GetFileName).ToArray();
- }
- else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.BluRay)
- {
- state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.BluRay).Select(Path.GetFileName).ToArray();
- }
- else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.Dvd)
- {
- state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.Dvd).Select(Path.GetFileName).ToArray();
- }
- else
- {
- state.PlayableStreamFileNames = Array.Empty<string>();
- }
- }
- else
- {
- state.PlayableStreamFileNames = Array.Empty<string>();
- }
-
if (mediaSource.Timestamp.HasValue)
{
state.InputTimestamp = mediaSource.Timestamp.Value;
@@ -2502,8 +3210,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";
@@ -2554,9 +3262,10 @@ namespace MediaBrowser.Controller.MediaEncoding
state.MediaSource = mediaSource;
var request = state.BaseRequest;
- if (!string.IsNullOrWhiteSpace(request.AudioCodec))
+ var supportedAudioCodecs = state.SupportedAudioCodecs;
+ if (request != null && supportedAudioCodecs != null && supportedAudioCodecs.Length > 0)
{
- var supportedAudioCodecsList = request.AudioCodec.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
+ var supportedAudioCodecsList = supportedAudioCodecs.ToList();
ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream);
@@ -2565,11 +3274,23 @@ namespace MediaBrowser.Controller.MediaEncoding
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
?? state.SupportedAudioCodecs.FirstOrDefault();
}
+
+ var supportedVideoCodecs = state.SupportedVideoCodecs;
+ if (request != null && supportedVideoCodecs != null && supportedVideoCodecs.Length > 0)
+ {
+ var supportedVideoCodecsList = supportedVideoCodecs.ToList();
+
+ ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions);
+
+ state.SupportedVideoCodecs = supportedVideoCodecsList.ToArray();
+
+ request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+ }
}
private void ShiftAudioCodecsIfNeeded(List<string> audioCodecs, MediaStream audioStream)
{
- // Nothing to do here
+ // No need to shift if there is only one supported audio codec.
if (audioCodecs.Count < 2)
{
return;
@@ -2597,6 +3318,34 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions)
+ {
+ // Shift hevc/h265 to the end of list if hevc encoding is not allowed.
+ if (encodingOptions.AllowHevcEncoding)
+ {
+ return;
+ }
+
+ // No need to shift if there is only one supported video codec.
+ if (videoCodecs.Count < 2)
+ {
+ return;
+ }
+
+ var shiftVideoCodecs = new[] { "hevc", "h265" };
+ if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ {
+ return;
+ }
+
+ while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparer.OrdinalIgnoreCase))
+ {
+ var removed = shiftVideoCodecs[0];
+ videoCodecs.RemoveAt(0);
+ videoCodecs.Add(removed);
+ }
+ }
+
private void NormalizeSubtitleEmbed(EncodingJobInfo state)
{
if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
@@ -2630,7 +3379,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
- // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+ // Since transcoding of folder rips is experimental anyway, it's not worth adding additional variables such as this.
if (videoType != VideoType.VideoFile)
{
return null;
@@ -2653,63 +3402,32 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
+ // Hybrid VPP tonemapping with VAAPI
+ if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)
+ && IsVppTonemappingSupported(state, encodingOptions))
+ {
+ // Since tonemap_vaapi only support HEVC for now, no need to check the codec again.
+ return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10);
+ }
+
if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
switch (videoStream.Codec.ToLowerInvariant())
{
case "avc":
case "h264":
- if (_mediaEncoder.SupportsDecoder("h264_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
- {
- // qsv decoder does not support 10-bit input
- if ((videoStream.BitDepth ?? 8) > 8)
- {
- encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
- return null;
- }
-
- return "-c:v h264_qsv";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "h264_qsv", "h264", isColorDepth10);
case "hevc":
case "h265":
- if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
- {
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : "-c:v hevc_qsv";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "hevc_qsv", "hevc", isColorDepth10);
case "mpeg2video":
- if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg2_qsv";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "mpeg2_qsv", "mpeg2video", isColorDepth10);
case "vc1":
- if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v vc1_qsv";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vc1_qsv", "vc1", isColorDepth10);
case "vp8":
- if (_mediaEncoder.SupportsDecoder("vp8_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v vp8_qsv";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vp8_qsv", "vp8", isColorDepth10);
case "vp9":
- if (_mediaEncoder.SupportsDecoder("vp9_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase))
- {
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : "-c:v vp9_qsv";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vp9_qsv", "vp9", isColorDepth10);
}
}
else if (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
@@ -2718,57 +3436,34 @@ namespace MediaBrowser.Controller.MediaEncoding
{
case "avc":
case "h264":
- if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v h264_cuvid";
- }
-
- break;
+ return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported()
+ ? GetHwaccelType(state, encodingOptions, "h264", isColorDepth10)
+ : GetHwDecoderName(encodingOptions, "h264_cuvid", "h264", isColorDepth10);
case "hevc":
case "h265":
- if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
- {
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : "-c:v hevc_cuvid";
- }
-
- break;
+ return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported()
+ ? GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10)
+ : GetHwDecoderName(encodingOptions, "hevc_cuvid", "hevc", isColorDepth10);
case "mpeg2video":
- if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg2_cuvid";
- }
-
- break;
+ return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported()
+ ? GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10)
+ : GetHwDecoderName(encodingOptions, "mpeg2_cuvid", "mpeg2video", isColorDepth10);
case "vc1":
- if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v vc1_cuvid";
- }
-
- break;
+ return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported()
+ ? GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10)
+ : GetHwDecoderName(encodingOptions, "vc1_cuvid", "vc1", isColorDepth10);
case "mpeg4":
- if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg4_cuvid";
- }
-
- break;
+ return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported()
+ ? GetHwaccelType(state, encodingOptions, "mpeg4", isColorDepth10)
+ : GetHwDecoderName(encodingOptions, "mpeg4_cuvid", "mpeg4", isColorDepth10);
case "vp8":
- if (_mediaEncoder.SupportsDecoder("vp8_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v vp8_cuvid";
- }
-
- break;
+ return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported()
+ ? GetHwaccelType(state, encodingOptions, "vp8", isColorDepth10)
+ : GetHwDecoderName(encodingOptions, "vp8_cuvid", "vp8", isColorDepth10);
case "vp9":
- if (_mediaEncoder.SupportsDecoder("vp9_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase))
- {
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : "-c:v vp9_cuvid";
- }
-
- break;
+ return encodingOptions.EnableEnhancedNvdecDecoder && IsCudaSupported()
+ ? GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10)
+ : GetHwDecoderName(encodingOptions, "vp9_cuvid", "vp9", isColorDepth10);
}
}
else if (string.Equals(encodingOptions.HardwareAccelerationType, "mediacodec", StringComparison.OrdinalIgnoreCase))
@@ -2777,50 +3472,18 @@ namespace MediaBrowser.Controller.MediaEncoding
{
case "avc":
case "h264":
- if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v h264_mediacodec";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "h264_mediacodec", "h264", isColorDepth10);
case "hevc":
case "h265":
- if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
- {
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : "-c:v hevc_mediacodec";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "hevc_mediacodec", "hevc", isColorDepth10);
case "mpeg2video":
- if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg2_mediacodec";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "mpeg2_mediacodec", "mpeg2video", isColorDepth10);
case "mpeg4":
- if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg4_mediacodec";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "mpeg4_mediacodec", "mpeg4", isColorDepth10);
case "vp8":
- if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v vp8_mediacodec";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vp8_mediacodec", "vp8", isColorDepth10);
case "vp9":
- if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase))
- {
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : "-c:v vp9_mediacodec";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vp9_mediacodec", "vp9", isColorDepth10);
}
}
else if (string.Equals(encodingOptions.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase))
@@ -2829,33 +3492,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
case "avc":
case "h264":
- if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v h264_mmal";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "h264_mmal", "h264", isColorDepth10);
case "mpeg2video":
- if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg2_mmal";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "mpeg2_mmal", "mpeg2video", isColorDepth10);
case "mpeg4":
- if (_mediaEncoder.SupportsDecoder("mpeg4_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg4_mmal";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "mpeg4_mmal", "mpeg4", isColorDepth10);
case "vc1":
- if (_mediaEncoder.SupportsDecoder("vc1_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v vc1_mmal";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vc1_mmal", "vc1", isColorDepth10);
}
}
else if (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
@@ -2864,20 +3507,18 @@ namespace MediaBrowser.Controller.MediaEncoding
{
case "avc":
case "h264":
- return GetHwaccelType(state, encodingOptions, "h264");
+ return GetHwaccelType(state, encodingOptions, "h264", isColorDepth10);
case "hevc":
case "h265":
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : GetHwaccelType(state, encodingOptions, "hevc");
+ return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10);
case "mpeg2video":
- return GetHwaccelType(state, encodingOptions, "mpeg2video");
+ return GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10);
case "vc1":
- return GetHwaccelType(state, encodingOptions, "vc1");
+ return GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10);
case "mpeg4":
- return GetHwaccelType(state, encodingOptions, "mpeg4");
+ return GetHwaccelType(state, encodingOptions, "mpeg4", isColorDepth10);
case "vp9":
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : GetHwaccelType(state, encodingOptions, "vp9");
+ return GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10);
}
}
else if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
@@ -2886,20 +3527,18 @@ namespace MediaBrowser.Controller.MediaEncoding
{
case "avc":
case "h264":
- return GetHwaccelType(state, encodingOptions, "h264");
+ return GetHwaccelType(state, encodingOptions, "h264", isColorDepth10);
case "hevc":
case "h265":
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : GetHwaccelType(state, encodingOptions, "hevc");
+ return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10);
case "mpeg2video":
- return GetHwaccelType(state, encodingOptions, "mpeg2video");
+ return GetHwaccelType(state, encodingOptions, "mpeg2video", isColorDepth10);
case "vc1":
- return GetHwaccelType(state, encodingOptions, "vc1");
+ return GetHwaccelType(state, encodingOptions, "vc1", isColorDepth10);
case "vp8":
- return GetHwaccelType(state, encodingOptions, "vp8");
+ return GetHwaccelType(state, encodingOptions, "vp8", isColorDepth10);
case "vp9":
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : GetHwaccelType(state, encodingOptions, "vp9");
+ return GetHwaccelType(state, encodingOptions, "vp9", isColorDepth10);
}
}
else if (string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
@@ -2908,62 +3547,25 @@ namespace MediaBrowser.Controller.MediaEncoding
{
case "avc":
case "h264":
- if (_mediaEncoder.SupportsDecoder("h264_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v h264_opencl";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "h264_opencl", "h264", isColorDepth10);
case "hevc":
case "h265":
- if (_mediaEncoder.SupportsDecoder("hevc_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
- {
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Hevc) ? null : "-c:v hevc_opencl";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "hevc_opencl", "hevc", isColorDepth10);
case "mpeg2video":
- if (_mediaEncoder.SupportsDecoder("mpeg2_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg2_opencl";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "mpeg2_opencl", "mpeg2video", isColorDepth10);
case "mpeg4":
- if (_mediaEncoder.SupportsDecoder("mpeg4_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v mpeg4_opencl";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "mpeg4_opencl", "mpeg4", isColorDepth10);
case "vc1":
- if (_mediaEncoder.SupportsDecoder("vc1_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v vc1_opencl";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vc1_opencl", "vc1", isColorDepth10);
case "vp8":
- if (_mediaEncoder.SupportsDecoder("vp8_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
- {
- return "-c:v vp8_opencl";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vp8_opencl", "vp8", isColorDepth10);
case "vp9":
- if (_mediaEncoder.SupportsDecoder("vp9_opencl") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
- {
- return (isColorDepth10 &&
- !encodingOptions.EnableDecodingColorDepth10Vp9) ? null : "-c:v vp9_opencl";
- }
-
- break;
+ return GetHwDecoderName(encodingOptions, "vp9_opencl", "vp9", isColorDepth10);
}
}
}
- var whichCodec = videoStream.Codec.ToLowerInvariant();
+ var whichCodec = videoStream.Codec?.ToLowerInvariant();
switch (whichCodec)
{
case "avc":
@@ -2982,30 +3584,88 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system
+ /// Gets a hw decoder name.
+ /// </summary>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="decoder">Decoder to use.</param>
+ /// <param name="videoCodec">Video codec to use.</param>
+ /// <param name="isColorDepth10">Specifies if color depth 10.</param>
+ /// <returns>Hardware decoder name.</returns>
+ public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10)
+ {
+ var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoder) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase);
+ if (isColorDepth10 && isCodecAvailable)
+ {
+ if ((options.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Hevc)
+ || (options.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Vp9))
+ {
+ return null;
+ }
+ }
+
+ return isCodecAvailable ? ("-c:v " + decoder) : null;
+ }
+
+ /// <summary>
+ /// 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)
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="videoCodec">Video codec to use.</param>
+ /// <param name="isColorDepth10">Specifies if color depth 10.</param>
+ /// <returns>Hardware accelerator type.</returns>
+ public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10)
{
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ var isWindows = OperatingSystem.IsWindows();
+ var isLinux = OperatingSystem.IsLinux();
var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
+ var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase);
- if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
+ if (isColorDepth10 && isCodecAvailable)
{
- if (isLinux)
+ if ((options.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Hevc)
+ || (options.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Vp9))
{
- return "-hwaccel vaapi";
+ return null;
}
+ }
- if (isWindows && isWindows8orLater)
+ if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
+ {
+ // Currently there is no AMF decoder on Linux, only have h264 encoder.
+ if (isDxvaSupported && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{
- return "-hwaccel d3d11va";
+ if (isWindows && isWindows8orLater)
+ {
+ return "-hwaccel d3d11va";
+ }
+
+ if (isWindows && !isWindows8orLater)
+ {
+ return "-hwaccel dxva2";
+ }
}
+ }
- if (isWindows && !isWindows8orLater)
+ if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
+ || (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)
+ && IsVppTonemappingSupported(state, options)))
+ {
+ if (IsVaapiSupported(state) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
+ {
+ if (isLinux)
+ {
+ return "-hwaccel vaapi";
+ }
+ }
+ }
+
+ if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ {
+ if (options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{
- return "-hwaccel dxva2";
+ return "-hwaccel cuda";
}
}
@@ -3078,7 +3738,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (flags.Count > 0)
{
- return " -fflags " + string.Join("", flags);
+ return " -fflags " + string.Join(string.Empty, flags);
}
return string.Empty;
@@ -3220,7 +3880,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
}
- args += " " + GetAudioFilterParam(state, encodingOptions, false);
+ args += GetAudioFilterParam(state, encodingOptions);
return args;
}
@@ -3261,7 +3921,7 @@ namespace MediaBrowser.Controller.MediaEncoding
GetInputArgument(state, encodingOptions),
threads,
" -vn",
- string.Join(" ", audioTranscodeParams),
+ string.Join(' ', audioTranscodeParams),
outputPath,
string.Empty,
string.Empty,
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 68bc502a0f..fa9f40d60d 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS1591, SA1401
using System;
using System.Collections.Generic;
@@ -9,7 +11,6 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
@@ -19,6 +20,44 @@ namespace MediaBrowser.Controller.MediaEncoding
// For now, a common base class until the API and MediaEncoding classes are unified
public class EncodingJobInfo
{
+ public int? OutputAudioBitrate;
+ public int? OutputAudioChannels;
+
+ private TranscodeReason[] _transcodeReasons = null;
+
+ public EncodingJobInfo(TranscodingJobType jobType)
+ {
+ TranscodingType = jobType;
+ RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SupportedAudioCodecs = Array.Empty<string>();
+ SupportedVideoCodecs = Array.Empty<string>();
+ SupportedSubtitleCodecs = Array.Empty<string>();
+ }
+
+ public TranscodeReason[] TranscodeReasons
+ {
+ get
+ {
+ if (_transcodeReasons == null)
+ {
+ if (BaseRequest.TranscodeReasons == null)
+ {
+ return Array.Empty<TranscodeReason>();
+ }
+
+ _transcodeReasons = BaseRequest.TranscodeReasons
+ .Split(',')
+ .Where(i => !string.IsNullOrEmpty(i))
+ .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
+ .ToArray();
+ }
+
+ return _transcodeReasons;
+ }
+ }
+
+ public IProgress<double> Progress { get; set; }
+
public MediaStream VideoStream { get; set; }
public VideoType VideoType { get; set; }
@@ -33,10 +72,6 @@ namespace MediaBrowser.Controller.MediaEncoding
public bool IsInputVideo { get; set; }
- public IIsoMount IsoMount { get; set; }
-
- public string[] PlayableStreamFileNames { get; set; }
-
public string OutputAudioCodec { get; set; }
public int? OutputVideoBitrate { get; set; }
@@ -61,39 +96,6 @@ namespace MediaBrowser.Controller.MediaEncoding
public string MimeType { get; set; }
- public string GetMimeType(string outputPath, bool enableStreamDefault = true)
- {
- if (!string.IsNullOrEmpty(MimeType))
- {
- return MimeType;
- }
-
- return MimeTypes.GetMimeType(outputPath, enableStreamDefault);
- }
-
- private TranscodeReason[] _transcodeReasons = null;
- public TranscodeReason[] TranscodeReasons
- {
- get
- {
- if (_transcodeReasons == null)
- {
- if (BaseRequest.TranscodeReasons == null)
- {
- return Array.Empty<TranscodeReason>();
- }
-
- _transcodeReasons = BaseRequest.TranscodeReasons
- .Split(',')
- .Where(i => !string.IsNullOrEmpty(i))
- .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
- .ToArray();
- }
-
- return _transcodeReasons;
- }
- }
-
public bool IgnoreInputDts => MediaSource.IgnoreDts;
public bool IgnoreInputIndex => MediaSource.IgnoreIndex;
@@ -146,192 +148,17 @@ namespace MediaBrowser.Controller.MediaEncoding
public BaseEncodingJobOptions BaseRequest { get; set; }
- public long? StartTimeTicks => BaseRequest.StartTimeTicks;
-
- public bool CopyTimestamps => BaseRequest.CopyTimestamps;
-
- public int? OutputAudioBitrate;
- public int? OutputAudioChannels;
-
- public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
- {
- var videoStream = VideoStream;
- var isInputInterlaced = videoStream != null && videoStream.IsInterlaced;
-
- if (!isInputInterlaced)
- {
- return false;
- }
-
- // Support general param
- if (BaseRequest.DeInterlace)
- {
- return true;
- }
-
- if (!string.IsNullOrEmpty(videoCodec))
- {
- if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return forceDeinterlaceIfSourceIsInterlaced && isInputInterlaced;
- }
-
- public string[] GetRequestedProfiles(string codec)
- {
- if (!string.IsNullOrEmpty(BaseRequest.Profile))
- {
- return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- var profile = BaseRequest.GetOption(codec, "profile");
-
- if (!string.IsNullOrEmpty(profile))
- {
- return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- return Array.Empty<string>();
- }
-
- public string GetRequestedLevel(string codec)
- {
- if (!string.IsNullOrEmpty(BaseRequest.Level))
- {
- return BaseRequest.Level;
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- return BaseRequest.GetOption(codec, "level");
- }
-
- return null;
- }
-
- public int? GetRequestedMaxRefFrames(string codec)
- {
- if (BaseRequest.MaxRefFrames.HasValue)
- {
- return BaseRequest.MaxRefFrames;
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "maxrefframes");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
- return null;
- }
-
- public int? GetRequestedVideoBitDepth(string codec)
- {
- if (BaseRequest.MaxVideoBitDepth.HasValue)
- {
- return BaseRequest.MaxVideoBitDepth;
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "videobitdepth");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
- return null;
- }
-
- public int? GetRequestedAudioBitDepth(string codec)
- {
- if (BaseRequest.MaxAudioBitDepth.HasValue)
- {
- return BaseRequest.MaxAudioBitDepth;
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "audiobitdepth");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
- return null;
- }
-
- public int? GetRequestedAudioChannels(string codec)
- {
- if (BaseRequest.MaxAudioChannels.HasValue)
- {
- return BaseRequest.MaxAudioChannels;
- }
-
- if (BaseRequest.AudioChannels.HasValue)
- {
- return BaseRequest.AudioChannels;
- }
-
- 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;
- }
-
public bool IsVideoRequest { get; set; }
public TranscodingJobType TranscodingType { get; set; }
- public EncodingJobInfo(TranscodingJobType jobType)
- {
- TranscodingType = jobType;
- RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- PlayableStreamFileNames = Array.Empty<string>();
- SupportedAudioCodecs = Array.Empty<string>();
- SupportedVideoCodecs = Array.Empty<string>();
- SupportedSubtitleCodecs = Array.Empty<string>();
- }
+ public long? StartTimeTicks => BaseRequest.StartTimeTicks;
+
+ public bool CopyTimestamps => BaseRequest.CopyTimestamps;
public bool IsSegmentedLiveStream
=> TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue;
- public bool EnableBreakOnNonKeyFrames(string videoCodec)
- {
- if (TranscodingType != TranscodingJobType.Progressive)
- {
- if (IsSegmentedLiveStream)
- {
- return false;
- }
-
- return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
- }
-
- return false;
- }
-
public int? TotalOutputBitrate => (OutputAudioBitrate ?? 0) + (OutputVideoBitrate ?? 0);
public int? OutputWidth
@@ -342,7 +169,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
- var newSize = DrawingUtils.Resize(size,
+ var newSize = DrawingUtils.Resize(
+ size,
BaseRequest.Width ?? 0,
BaseRequest.Height ?? 0,
BaseRequest.MaxWidth ?? 0,
@@ -368,7 +196,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
- var newSize = DrawingUtils.Resize(size,
+ var newSize = DrawingUtils.Resize(
+ size,
BaseRequest.Width ?? 0,
BaseRequest.Height ?? 0,
BaseRequest.MaxWidth ?? 0,
@@ -402,7 +231,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Don't exceed what the encoder supports
// Seeing issues of attempting to encode to 88200
- return Math.Min(44100, BaseRequest.AudioSampleRate.Value);
+ return BaseRequest.AudioSampleRate.Value;
}
return null;
@@ -427,7 +256,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target video level.
/// </summary>
public double? TargetVideoLevel
{
@@ -450,7 +279,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target video bit depth.
/// </summary>
public int? TargetVideoBitDepth
{
@@ -485,7 +314,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target framerate.
/// </summary>
public float? TargetFramerate
{
@@ -517,7 +346,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target packet length.
/// </summary>
public int? TargetPacketLength
{
@@ -533,7 +362,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Predicts the audio sample rate that will be in the output stream.
+ /// Gets the target video profile.
/// </summary>
public string TargetVideoProfile
{
@@ -586,6 +415,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
get
{
+ if (VideoStream == null)
+ {
+ return null;
+ }
+
if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
{
return VideoStream?.Codec;
@@ -599,6 +433,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
get
{
+ if (AudioStream == null)
+ {
+ return null;
+ }
+
if (EncodingHelper.IsCopyCodec(OutputAudioCodec))
{
return AudioStream?.Codec;
@@ -668,6 +507,21 @@ namespace MediaBrowser.Controller.MediaEncoding
public int HlsListSize => 0;
+ public bool EnableBreakOnNonKeyFrames(string videoCodec)
+ {
+ if (TranscodingType != TranscodingJobType.Progressive)
+ {
+ if (IsSegmentedLiveStream)
+ {
+ return false;
+ }
+
+ return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
+ }
+
+ return false;
+ }
+
private int? GetMediaStreamCount(MediaStreamType type, int limit)
{
var count = MediaSource.GetStreamCount(type);
@@ -680,30 +534,171 @@ namespace MediaBrowser.Controller.MediaEncoding
return count;
}
- public IProgress<double> Progress { get; set; }
+ public string GetMimeType(string outputPath, bool enableStreamDefault = true)
+ {
+ if (!string.IsNullOrEmpty(MimeType))
+ {
+ return MimeType;
+ }
+
+ return MimeTypes.GetMimeType(outputPath, enableStreamDefault);
+ }
+
+ public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
+ {
+ var videoStream = VideoStream;
+ var isInputInterlaced = videoStream != null && videoStream.IsInterlaced;
+
+ if (!isInputInterlaced)
+ {
+ return false;
+ }
+
+ // Support general param
+ if (BaseRequest.DeInterlace)
+ {
+ return true;
+ }
+
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return forceDeinterlaceIfSourceIsInterlaced && isInputInterlaced;
+ }
+
+ public string[] GetRequestedProfiles(string codec)
+ {
+ if (!string.IsNullOrEmpty(BaseRequest.Profile))
+ {
+ return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var profile = BaseRequest.GetOption(codec, "profile");
+
+ if (!string.IsNullOrEmpty(profile))
+ {
+ return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+ }
+
+ return Array.Empty<string>();
+ }
+
+ public string GetRequestedLevel(string codec)
+ {
+ if (!string.IsNullOrEmpty(BaseRequest.Level))
+ {
+ return BaseRequest.Level;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ return BaseRequest.GetOption(codec, "level");
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedMaxRefFrames(string codec)
+ {
+ if (BaseRequest.MaxRefFrames.HasValue)
+ {
+ return BaseRequest.MaxRefFrames;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "maxrefframes");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedVideoBitDepth(string codec)
+ {
+ if (BaseRequest.MaxVideoBitDepth.HasValue)
+ {
+ return BaseRequest.MaxVideoBitDepth;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "videobitdepth");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedAudioBitDepth(string codec)
+ {
+ if (BaseRequest.MaxAudioBitDepth.HasValue)
+ {
+ return BaseRequest.MaxAudioBitDepth;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "audiobitdepth");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ 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;
+ }
+
+ if (BaseRequest.AudioChannels.HasValue)
+ {
+ return BaseRequest.AudioChannels;
+ }
+
+ if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
+ {
+ return BaseRequest.TranscodingMaxAudioChannels;
+ }
+
+ return null;
+ }
public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
{
Progress.Report(percentComplete.Value);
}
}
-
- /// <summary>
- /// Enum TranscodingJobType.
- /// </summary>
- public enum TranscodingJobType
- {
- /// <summary>
- /// The progressive.
- /// </summary>
- Progressive,
- /// <summary>
- /// The HLS.
- /// </summary>
- Hls,
- /// <summary>
- /// The dash.
- /// </summary>
- Dash
- }
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
deleted file mode 100644
index 4cbb63e46c..0000000000
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Controller.MediaEncoding
-{
- public class EncodingJobOptions : BaseEncodingJobOptions
- {
- public string OutputDirectory { get; set; }
-
- public string ItemId { get; set; }
-
- public string TempDirectory { get; set; }
-
- public bool ReadInputAtNativeFramerate { get; set; }
-
- /// <summary>
- /// Gets a value indicating whether this instance has fixed resolution.
- /// </summary>
- /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
- public bool HasFixedResolution => Width.HasValue || Height.HasValue;
-
- public DeviceProfile DeviceProfile { get; set; }
-
- public EncodingJobOptions(StreamInfo info, DeviceProfile deviceProfile)
- {
- Container = info.Container;
- StartTimeTicks = info.StartPositionTicks;
- MaxWidth = info.MaxWidth;
- MaxHeight = info.MaxHeight;
- MaxFramerate = info.MaxFramerate;
- Id = info.ItemId;
- MediaSourceId = info.MediaSourceId;
- AudioCodec = info.TargetAudioCodec.FirstOrDefault();
- MaxAudioChannels = info.GlobalMaxAudioChannels;
- AudioBitRate = info.AudioBitrate;
- AudioSampleRate = info.TargetAudioSampleRate;
- DeviceProfile = deviceProfile;
- VideoCodec = info.TargetVideoCodec.FirstOrDefault();
- VideoBitRate = info.VideoBitrate;
- AudioStreamIndex = info.AudioStreamIndex;
- SubtitleMethod = info.SubtitleDeliveryMethod;
- Context = info.Context;
- TranscodingMaxAudioChannels = info.TranscodingMaxAudioChannels;
-
- if (info.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External)
- {
- SubtitleStreamIndex = info.SubtitleStreamIndex;
- }
-
- StreamOptions = info.StreamOptions;
- }
- }
-
- // For now until api and media encoding layers are unified
- public class BaseEncodingJobOptions
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string MediaSourceId { get; set; }
-
- [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string DeviceId { get; set; }
-
- [ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Container { get; set; }
-
- /// <summary>
- /// Gets or sets the audio codec.
- /// </summary>
- /// <value>The audio codec.</value>
- [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string AudioCodec { get; set; }
-
- [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool EnableAutoStreamCopy { get; set; }
-
- public bool AllowVideoStreamCopy { get; set; }
-
- public bool AllowAudioStreamCopy { get; set; }
-
- public bool BreakOnNonKeyFrames { get; set; }
-
- /// <summary>
- /// Gets or sets the audio sample rate.
- /// </summary>
- /// <value>The audio sample rate.</value>
- [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? AudioSampleRate { get; set; }
-
- public int? MaxAudioBitDepth { get; set; }
-
- /// <summary>
- /// Gets or sets the audio bit rate.
- /// </summary>
- /// <value>The audio bit rate.</value>
- [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? AudioBitRate { get; set; }
-
- /// <summary>
- /// Gets or sets the audio channels.
- /// </summary>
- /// <value>The audio channels.</value>
- [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? AudioChannels { get; set; }
-
- [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? MaxAudioChannels { get; set; }
-
- [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool Static { get; set; }
-
- /// <summary>
- /// Gets or sets the profile.
- /// </summary>
- /// <value>The profile.</value>
- [ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Profile { get; set; }
-
- /// <summary>
- /// Gets or sets the level.
- /// </summary>
- /// <value>The level.</value>
- [ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string Level { get; set; }
-
- /// <summary>
- /// Gets or sets the framerate.
- /// </summary>
- /// <value>The framerate.</value>
- [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
- public float? Framerate { get; set; }
-
- [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
- public float? MaxFramerate { get; set; }
-
- [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
- public bool CopyTimestamps { get; set; }
-
- /// <summary>
- /// Gets or sets the start time ticks.
- /// </summary>
- /// <value>The start time ticks.</value>
- [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public long? StartTimeTicks { get; set; }
-
- /// <summary>
- /// Gets or sets the width.
- /// </summary>
- /// <value>The width.</value>
- [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Width { get; set; }
-
- /// <summary>
- /// Gets or sets the height.
- /// </summary>
- /// <value>The height.</value>
- [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? Height { get; set; }
-
- /// <summary>
- /// Gets or sets the width of the max.
- /// </summary>
- /// <value>The width of the max.</value>
- [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? MaxWidth { get; set; }
-
- /// <summary>
- /// Gets or sets the height of the max.
- /// </summary>
- /// <value>The height of the max.</value>
- [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? MaxHeight { get; set; }
-
- /// <summary>
- /// Gets or sets the video bit rate.
- /// </summary>
- /// <value>The video bit rate.</value>
- [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? VideoBitRate { get; set; }
-
- /// <summary>
- /// Gets or sets the index of the subtitle stream.
- /// </summary>
- /// <value>The index of the subtitle stream.</value>
- [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? SubtitleStreamIndex { get; set; }
-
- [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public SubtitleDeliveryMethod SubtitleMethod { get; set; }
-
- [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? MaxRefFrames { get; set; }
-
- [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? MaxVideoBitDepth { get; set; }
-
- public bool RequireAvc { get; set; }
-
- public bool DeInterlace { get; set; }
-
- public bool RequireNonAnamorphic { get; set; }
-
- public int? TranscodingMaxAudioChannels { get; set; }
-
- public int? CpuCoreLimit { get; set; }
-
- public string LiveStreamId { get; set; }
-
- public bool EnableMpegtsM2TsMode { get; set; }
-
- /// <summary>
- /// Gets or sets the video codec.
- /// </summary>
- /// <value>The video codec.</value>
- [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string VideoCodec { get; set; }
-
- public string SubtitleCodec { get; set; }
-
- public string TranscodeReasons { get; set; }
-
- /// <summary>
- /// Gets or sets the index of the audio stream.
- /// </summary>
- /// <value>The index of the audio stream.</value>
- [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? AudioStreamIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the index of the video stream.
- /// </summary>
- /// <value>The index of the video stream.</value>
- [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int? VideoStreamIndex { get; set; }
-
- public EncodingContext Context { get; set; }
-
- public Dictionary<string, string> StreamOptions { get; set; }
-
- public string GetOption(string qualifier, string name)
- {
- var value = GetOption(qualifier + "-" + name);
-
- if (string.IsNullOrEmpty(value))
- {
- value = GetOption(name);
- }
-
- return value;
- }
-
- public string GetOption(string name)
- {
- if (StreamOptions.TryGetValue(name, out var value))
- {
- return value;
- }
-
- return null;
- }
-
- public BaseEncodingJobOptions()
- {
- EnableAutoStreamCopy = true;
- AllowVideoStreamCopy = true;
- AllowAudioStreamCopy = true;
- Context = EncodingContext.Streaming;
- StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
- }
-}
diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
index fbc8275341..c38e7ec3b3 100644
--- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
index 15a2580afd..8ce40a58d1 100644
--- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -14,6 +16,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Refreshes the chapter images.
/// </summary>
+ /// <param name="video">Video to use.</param>
+ /// <param name="directoryService">Directory service to use.</param>
+ /// <param name="chapters">Set of chapters to refresh.</param>
+ /// <param name="extractImages">Option to extract images.</param>
+ /// <param name="saveChapters">Option to save chapters.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns><c>true</c> if successful, <c>false</c> if not.</returns>
Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 17d6dc5d27..ff24560703 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -5,8 +7,8 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.System;
@@ -18,7 +20,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public interface IMediaEncoder : ITranscoderSupport
{
/// <summary>
- /// The location of the discovered FFmpeg tool.
+ /// Gets location of the discovered FFmpeg tool.
/// </summary>
FFmpegLocation EncoderLocation { get; }
@@ -50,6 +52,14 @@ namespace MediaBrowser.Controller.MediaEncoding
bool SupportsHwaccel(string hwaccel);
/// <summary>
+ /// Whether given filter is supported.
+ /// </summary>
+ /// <param name="filter">The filter.</param>
+ /// <param name="option">The option.</param>
+ /// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
+ bool SupportsFilter(string filter, string option);
+
+ /// <summary>
/// Extracts the audio image.
/// </summary>
/// <param name="path">The path.</param>
@@ -61,17 +71,47 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Extracts the video image.
/// </summary>
- Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken);
+ /// <param name="inputFile">Input file.</param>
+ /// <param name="container">Video container type.</param>
+ /// <param name="mediaSource">Media source information.</param>
+ /// <param name="videoStream">Media stream information.</param>
+ /// <param name="threedFormat">Video 3D format.</param>
+ /// <param name="offset">Time offset.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Location of video image.</returns>
+ Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken);
- Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken);
+ /// <summary>
+ /// Extracts the video image.
+ /// </summary>
+ /// <param name="inputFile">Input file.</param>
+ /// <param name="container">Video container type.</param>
+ /// <param name="mediaSource">Media source information.</param>
+ /// <param name="imageStream">Media stream information.</param>
+ /// <param name="imageStreamIndex">Index of the stream to extract from.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Location of video image.</returns>
+ Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken);
/// <summary>
/// Extracts the video images on interval.
/// </summary>
- Task ExtractVideoImagesOnInterval(string[] inputFiles,
+ /// <param name="inputFile">Input file.</param>
+ /// <param name="container">Video container type.</param>
+ /// <param name="videoStream">Media stream information.</param>
+ /// <param name="mediaSource">Media source information.</param>
+ /// <param name="threedFormat">Video 3D format.</param>
+ /// <param name="interval">Time interval.</param>
+ /// <param name="targetDirectory">Directory to write images.</param>
+ /// <param name="filenamePrefix">Filename prefix to use.</param>
+ /// <param name="maxWidth">Maximum width of image.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>A task.</returns>
+ Task ExtractVideoImagesOnInterval(
+ string inputFile,
string container,
MediaStream videoStream,
- MediaProtocol protocol,
+ MediaSourceInfo mediaSource,
Video3DFormat? threedFormat,
TimeSpan interval,
string targetDirectory,
@@ -90,10 +130,10 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the input argument.
/// </summary>
- /// <param name="inputFiles">The input files.</param>
- /// <param name="protocol">The protocol.</param>
+ /// <param name="inputFile">The input file.</param>
+ /// <param name="mediaSource">The mediaSource.</param>
/// <returns>System.String.</returns>
- string GetInputArgument(IReadOnlyList<string> inputFiles, MediaProtocol protocol);
+ string GetInputArgument(string inputFile, MediaSourceInfo mediaSource);
/// <summary>
/// Gets the time parameter.
@@ -111,10 +151,24 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.String.</returns>
string EscapeSubtitleFilterPath(string path);
+ /// <summary>
+ /// Sets the path to find FFmpeg.
+ /// </summary>
void SetFFmpegPath();
+ /// <summary>
+ /// Updates the encoder path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="pathType">The type of path.</param>
void UpdateEncoderPath(string path, string pathType);
- IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber);
+ /// <summary>
+ /// Gets the primary playlist of .vob files.
+ /// </summary>
+ /// <param name="path">The to the .vob files.</param>
+ /// <param name="titleNumber">The title number to start with.</param>
+ /// <returns>A playlist.</returns>
+ IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
index 6ebf7f159b..4483cf708a 100644
--- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
@@ -13,6 +15,14 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the subtitles.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="mediaSourceId">Media source.</param>
+ /// <param name="subtitleStreamIndex">Subtitle stream to use.</param>
+ /// <param name="outputFormat">Output format to use.</param>
+ /// <param name="startTimeTicks">Start time.</param>
+ /// <param name="endTimeTicks">End time.</param>
+ /// <param name="preserveOriginalTimestamps">Option to preserve original timestamps.</param>
+ /// <param name="cancellationToken">The cancellation token for the operation.</param>
/// <returns>Task{Stream}.</returns>
Task<Stream> GetSubtitles(
BaseItem item,
diff --git a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
index e7b4c8c15c..044ba6d331 100644
--- a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.MediaEncoding
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index ac520c5c44..b23c951127 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -1,10 +1,12 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -93,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase))
{
- var rate = part.Split(new[] { '=' }, 2)[^1];
+ var rate = part.Split('=', 2)[^1];
if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
{
@@ -103,7 +105,7 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (state.RunTimeTicks.HasValue &&
part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
{
- var time = part.Split(new[] { '=' }, 2).Last();
+ var time = part.Split('=', 2)[^1];
if (TimeSpan.TryParse(time, _usCulture, out var val))
{
@@ -116,7 +118,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
{
- var size = part.Split(new[] { '=' }, 2).Last();
+ var size = part.Split('=', 2)[^1];
int? scale = null;
if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
@@ -135,7 +137,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
{
- var rate = part.Split(new[] { '=' }, 2).Last();
+ var rate = part.Split('=', 2)[^1];
int? scale = null;
if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
index ce53c23ad2..841e7b2872 100644
--- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
+++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
@@ -1,11 +1,5 @@
#pragma warning disable CS1591
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using MediaBrowser.Model.IO;
-
namespace MediaBrowser.Controller.MediaEncoding
{
/// <summary>
@@ -13,38 +7,5 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
public static class MediaEncoderHelpers
{
- /// <summary>
- /// Gets the input argument.
- /// </summary>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="videoPath">The video path.</param>
- /// <param name="isoMount">The iso mount.</param>
- /// <param name="playableStreamFileNames">The playable stream file names.</param>
- /// <returns>string[].</returns>
- public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, IIsoMount isoMount, IReadOnlyCollection<string> playableStreamFileNames)
- {
- if (playableStreamFileNames.Count > 0)
- {
- if (isoMount == null)
- {
- return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames);
- }
-
- return GetPlayableStreamFiles(fileSystem, isoMount.MountedPath, playableStreamFileNames);
- }
-
- return new[] { videoPath };
- }
-
- private static string[] GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, IEnumerable<string> filenames)
- {
- var allFiles = fileSystem
- .GetFilePaths(rootPath, true)
- .ToArray();
-
- return filenames.Select(name => allFiles.FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase)))
- .Where(f => !string.IsNullOrEmpty(f))
- .ToArray();
- }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
index 59729de49c..1dd8bcf31b 100644
--- a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
+++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
@@ -1,9 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
-using System;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.IO;
namespace MediaBrowser.Controller.MediaEncoding
{
@@ -14,14 +14,5 @@ namespace MediaBrowser.Controller.MediaEncoding
public bool ExtractChapters { get; set; }
public DlnaProfileType MediaType { get; set; }
-
- public IIsoMount MountedIso { get; set; }
-
- public string[] PlayableStreamFileNames { get; set; }
-
- public MediaInfoRequest()
- {
- PlayableStreamFileNames = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs
new file mode 100644
index 0000000000..66b6283719
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ /// <summary>
+ /// Enum TranscodingJobType.
+ /// </summary>
+ public enum TranscodingJobType
+ {
+ /// <summary>
+ /// The progressive.
+ /// </summary>
+ Progressive,
+
+ /// <summary>
+ /// The HLS.
+ /// </summary>
+ Hls,
+
+ /// <summary>
+ /// The dash.
+ /// </summary>
+ Dash
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
deleted file mode 100644
index 1366fd42e8..0000000000
--- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace MediaBrowser.Controller.Net
-{
- public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes
- {
- public static IAuthService AuthService { get; set; }
-
- /// <summary>
- /// Gets or sets the roles.
- /// </summary>
- /// <value>The roles.</value>
- public string Roles { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [escape parental control].
- /// </summary>
- /// <value><c>true</c> if [escape parental control]; otherwise, <c>false</c>.</value>
- public bool EscapeParentalControl { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [allow before startup wizard].
- /// </summary>
- /// <value><c>true</c> if [allow before startup wizard]; otherwise, <c>false</c>.</value>
- public bool AllowBeforeStartupWizard { get; set; }
-
- public bool AllowLocal { get; set; }
-
- /// <summary>
- /// The request filter is executed before the service.
- /// </summary>
- /// <param name="request">The http request wrapper.</param>
- /// <param name="response">The http response wrapper.</param>
- /// <param name="requestDto">The request DTO.</param>
- public void RequestFilter(IRequest request, HttpResponse response, object requestDto)
- {
- AuthService.Authenticate(request, this);
- }
-
- /// <summary>
- /// Order in which Request Filters are executed.
- /// &lt;0 Executed before global request filters
- /// &gt;0 Executed after global request filters
- /// </summary>
- /// <value>The priority.</value>
- public int Priority => 0;
-
- public string[] GetRoles()
- {
- return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public bool IgnoreLegacyAuth { get; set; }
-
- public bool AllowLocalOnly { get; set; }
- }
-
- public interface IAuthenticationAttributes
- {
- bool EscapeParentalControl { get; }
-
- bool AllowBeforeStartupWizard { get; }
-
- bool AllowLocal { get; }
-
- bool AllowLocalOnly { get; }
-
- string[] GetRoles();
-
- bool IgnoreLegacyAuth { get; }
- }
-}
diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
index 735c46ef86..2452b25ab1 100644
--- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs
+++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
@@ -1,10 +1,13 @@
-#pragma warning disable CS1591
+#nullable disable
using System;
using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// The request authorization info.
+ /// </summary>
public class AuthorizationInfo
{
/// <summary>
@@ -43,6 +46,24 @@ namespace MediaBrowser.Controller.Net
/// <value>The token.</value>
public string Token { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the authorization is from an api key.
+ /// </summary>
+ public bool IsApiKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user making the request.
+ /// </summary>
public User User { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the token is authenticated.
+ /// </summary>
+ public bool IsAuthenticated { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the request has a token.
+ /// </summary>
+ public bool HasToken { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 916dea58be..0813a8e7d5 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS1591, SA1306, SA1401
using System;
using System.Collections.Generic;
@@ -8,6 +10,7 @@ using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Net
@@ -28,18 +31,6 @@ namespace MediaBrowser.Controller.Net
new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
/// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- protected abstract string Name { get; }
-
- /// <summary>
- /// Gets the data to send.
- /// </summary>
- /// <returns>Task{`1}.</returns>
- protected abstract Task<TReturnDataType> GetDataToSend();
-
- /// <summary>
/// The logger.
/// </summary>
protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
@@ -55,6 +46,30 @@ namespace MediaBrowser.Controller.Net
}
/// <summary>
+ /// Gets the type used for the messages sent to the client.
+ /// </summary>
+ /// <value>The type.</value>
+ protected abstract SessionMessageType Type { get; }
+
+ /// <summary>
+ /// Gets the message type received from the client to start sending messages.
+ /// </summary>
+ /// <value>The type.</value>
+ protected abstract SessionMessageType StartType { get; }
+
+ /// <summary>
+ /// Gets the message type received from the client to stop sending messages.
+ /// </summary>
+ /// <value>The type.</value>
+ protected abstract SessionMessageType StopType { get; }
+
+ /// <summary>
+ /// Gets the data to send.
+ /// </summary>
+ /// <returns>Task{`1}.</returns>
+ protected abstract Task<TReturnDataType> GetDataToSend();
+
+ /// <summary>
/// Processes the message.
/// </summary>
/// <param name="message">The message.</param>
@@ -66,12 +81,12 @@ namespace MediaBrowser.Controller.Net
throw new ArgumentNullException(nameof(message));
}
- if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase))
+ if (message.MessageType == StartType)
{
Start(message);
}
- if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase))
+ if (message.MessageType == StopType)
{
Stop(message);
}
@@ -79,6 +94,9 @@ namespace MediaBrowser.Controller.Net
return Task.CompletedTask;
}
+ /// <inheritdoc />
+ public Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection) => Task.CompletedTask;
+
/// <summary>
/// Starts sending messages over a web socket.
/// </summary>
@@ -159,7 +177,7 @@ namespace MediaBrowser.Controller.Net
new WebSocketMessage<TReturnDataType>
{
MessageId = Guid.NewGuid(),
- MessageType = Name,
+ MessageType = Type,
Data = data
},
cancellationToken).ConfigureAwait(false);
@@ -176,7 +194,7 @@ namespace MediaBrowser.Controller.Net
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error sending web socket message {Name}", Name);
+ Logger.LogError(ex, "Error sending web socket message {Name}", Type);
DisposeConnection(tuple);
}
}
@@ -252,13 +270,4 @@ namespace MediaBrowser.Controller.Net
GC.SuppressFinalize(this);
}
}
-
- public class WebSocketListenerState
- {
- public DateTime DateLastSendUtc { get; set; }
-
- public long InitialDelayMs { get; set; }
-
- public long IntervalMs { get; set; }
- }
}
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
index 2055a656a7..d15c6d3183 100644
--- a/MediaBrowser.Controller/Net/IAuthService.cs
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -1,7 +1,3 @@
-#nullable enable
-
-using Jellyfin.Data.Entities;
-using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@@ -12,21 +8,6 @@ namespace MediaBrowser.Controller.Net
public interface IAuthService
{
/// <summary>
- /// Authenticate and authorize request.
- /// </summary>
- /// <param name="request">Request.</param>
- /// <param name="authAttribtutes">Authorization attributes.</param>
- void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
-
- /// <summary>
- /// Authenticate and authorize request.
- /// </summary>
- /// <param name="request">Request.</param>
- /// <param name="authAttribtutes">Authorization attributes.</param>
- /// <returns>Authenticated user.</returns>
- User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
-
- /// <summary>
/// Authenticate request.
/// </summary>
/// <param name="request">The request.</param>
diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
index 37a7425b9d..0d310548dc 100644
--- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs
+++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
@@ -1,4 +1,3 @@
-using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@@ -13,14 +12,7 @@ namespace MediaBrowser.Controller.Net
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
- AuthorizationInfo GetAuthorizationInfo(object requestContext);
-
- /// <summary>
- /// Gets the authorization information.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <returns>AuthorizationInfo.</returns>
- AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
+ AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext);
/// <summary>
/// Gets the authorization information.
diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs
deleted file mode 100644
index b8cf8cd786..0000000000
--- a/MediaBrowser.Controller/Net/IHasResultFactory.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Controller.Net
-{
- /// <summary>
- /// Interface IHasResultFactory
- /// Services that require a ResultFactory should implement this
- /// </summary>
- public interface IHasResultFactory : IRequiresRequest
- {
- /// <summary>
- /// Gets or sets the result factory.
- /// </summary>
- /// <value>The result factory.</value>
- IHttpResultFactory ResultFactory { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs
deleted file mode 100644
index 8293a87142..0000000000
--- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace MediaBrowser.Controller.Net
-{
- /// <summary>
- /// Interface IHttpResultFactory.
- /// </summary>
- public interface IHttpResultFactory
- {
- /// <summary>
- /// Gets the result.
- /// </summary>
- /// <param name="content">The content.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <returns>System.Object.</returns>
- object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null);
-
- object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null);
-
- object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null);
-
- object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null);
-
- object GetRedirectResult(string url);
-
- object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
- where T : class;
-
- /// <summary>
- /// Gets the static result.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <param name="cacheKey">The cache key.</param>
- /// <param name="lastDateModified">The last date modified.</param>
- /// <param name="cacheDuration">Duration of the cache.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="factoryFn">The factory fn.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <returns>System.Object.</returns>
- Task<object> GetStaticResult(IRequest requestContext,
- Guid cacheKey,
- DateTime? lastDateModified,
- TimeSpan? cacheDuration,
- string contentType, Func<Task<Stream>> factoryFn,
- IDictionary<string, string> responseHeaders = null,
- bool isHeadRequest = false);
-
- /// <summary>
- /// Gets the static result.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <param name="options">The options.</param>
- /// <returns>System.Object.</returns>
- Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options);
-
- /// <summary>
- /// Gets the static file result.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <param name="path">The path.</param>
- /// <param name="fileShare">The file share.</param>
- /// <returns>System.Object.</returns>
- Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read);
-
- /// <summary>
- /// Gets the static file result.
- /// </summary>
- /// <param name="requestContext">The request context.</param>
- /// <param name="options">The options.</param>
- /// <returns>System.Object.</returns>
- Task<object> GetStaticFileResult(IRequest requestContext,
- StaticFileResultOptions options);
- }
-}
diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs
deleted file mode 100644
index b04ebda8ca..0000000000
--- a/MediaBrowser.Controller/Net/IHttpServer.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Jellyfin.Data.Events;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace MediaBrowser.Controller.Net
-{
- /// <summary>
- /// Interface IHttpServer.
- /// </summary>
- public interface IHttpServer
- {
- /// <summary>
- /// Gets the URL prefix.
- /// </summary>
- /// <value>The URL prefix.</value>
- string[] UrlPrefixes { get; }
-
- /// <summary>
- /// Occurs when [web socket connected].
- /// </summary>
- event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
- /// <summary>
- /// Inits this instance.
- /// </summary>
- void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes);
-
- /// <summary>
- /// If set, all requests will respond with this message.
- /// </summary>
- string GlobalResponse { get; set; }
-
- /// <summary>
- /// The HTTP request handler.
- /// </summary>
- /// <param name="context"></param>
- /// <returns></returns>
- Task RequestHandler(HttpContext context);
-
- /// <summary>
- /// Get the default CORS headers.
- /// </summary>
- /// <param name="req"></param>
- /// <returns></returns>
- IDictionary<string, string> GetDefaultCorsHeaders(IRequest req);
- }
-}
diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs
index 5da748f41d..6b896b41f4 100644
--- a/MediaBrowser.Controller/Net/ISessionContext.cs
+++ b/MediaBrowser.Controller/Net/ISessionContext.cs
@@ -2,7 +2,7 @@
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
@@ -10,10 +10,10 @@ namespace MediaBrowser.Controller.Net
{
SessionInfo GetSession(object requestContext);
- User GetUser(object requestContext);
+ User? GetUser(object requestContext);
- SessionInfo GetSession(IRequest requestContext);
+ SessionInfo GetSession(HttpContext requestContext);
- User GetUser(IRequest requestContext);
+ User? GetUser(HttpContext requestContext);
}
}
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index e87f3bca68..c8c5caf809 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -1,7 +1,5 @@
#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Net;
using System.Net.WebSockets;
@@ -32,7 +30,7 @@ namespace MediaBrowser.Controller.Net
DateTime LastKeepAliveDate { get; set; }
/// <summary>
- /// Gets or sets the query string.
+ /// Gets the query string.
/// </summary>
/// <value>The query string.</value>
IQueryCollection QueryString { get; }
@@ -58,11 +56,11 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// Sends a message asynchronously.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of websocket message data.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">message</exception>
+ /// <exception cref="ArgumentNullException">The message is null.</exception>
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
Task ProcessAsync(CancellationToken cancellationToken = default);
diff --git a/MediaBrowser.Controller/Net/IWebSocketListener.cs b/MediaBrowser.Controller/Net/IWebSocketListener.cs
index 7250a57b0a..f1a75d5180 100644
--- a/MediaBrowser.Controller/Net/IWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketListener.cs
@@ -3,7 +3,7 @@ using System.Threading.Tasks;
namespace MediaBrowser.Controller.Net
{
/// <summary>
- ///This is an interface for listening to messages coming through a web socket connection.
+ /// Interface for listening to messages coming through a web socket connection.
/// </summary>
public interface IWebSocketListener
{
@@ -13,5 +13,12 @@ namespace MediaBrowser.Controller.Net
/// <param name="message">The message.</param>
/// <returns>Task.</returns>
Task ProcessMessageAsync(WebSocketMessageInfo message);
+
+ /// <summary>
+ /// Processes a new web socket connection.
+ /// </summary>
+ /// <param name="connection">An instance of the <see cref="IWebSocketConnection"/> interface.</param>
+ /// <returns>Task.</returns>
+ Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection);
}
}
diff --git a/MediaBrowser.Controller/Net/IWebSocketManager.cs b/MediaBrowser.Controller/Net/IWebSocketManager.cs
new file mode 100644
index 0000000000..bb0ae83bea
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IWebSocketManager.cs
@@ -0,0 +1,18 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace MediaBrowser.Controller.Net
+{
+ /// <summary>
+ /// Interface IHttpServer.
+ /// </summary>
+ public interface IWebSocketManager
+ {
+ /// <summary>
+ /// The HTTP request handler.
+ /// </summary>
+ /// <param name="context">The current HTTP context.</param>
+ /// <returns>The task.</returns>
+ Task WebSocketRequestHandler(HttpContext context);
+ }
+}
diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs
index c6347133a8..f0d0b45a0a 100644
--- a/MediaBrowser.Controller/Net/SecurityException.cs
+++ b/MediaBrowser.Controller/Net/SecurityException.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
namespace MediaBrowser.Controller.Net
diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs
deleted file mode 100644
index c1e9bc8453..0000000000
--- a/MediaBrowser.Controller/Net/StaticResultOptions.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Net
-{
- public class StaticResultOptions
- {
- public string ContentType { get; set; }
-
- public TimeSpan? CacheDuration { get; set; }
-
- public DateTime? DateLastModified { get; set; }
-
- public Func<Task<Stream>> ContentFactory { get; set; }
-
- public bool IsHeadRequest { get; set; }
-
- public IDictionary<string, string> ResponseHeaders { get; set; }
-
- public Action OnComplete { get; set; }
-
- public Action OnError { get; set; }
-
- public string Path { get; set; }
-
- public long? ContentLength { get; set; }
-
- public FileShare FileShare { get; set; }
-
- public StaticResultOptions()
- {
- ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- FileShare = FileShare.Read;
- }
- }
-
- public class StaticFileResultOptions : StaticResultOptions
- {
- }
-}
diff --git a/MediaBrowser.Controller/Net/WebSocketListenerState.cs b/MediaBrowser.Controller/Net/WebSocketListenerState.cs
new file mode 100644
index 0000000000..70604d60a0
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketListenerState.cs
@@ -0,0 +1,17 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Controller.Net
+{
+ public class WebSocketListenerState
+ {
+ public DateTime DateLastSendUtc { get; set; }
+
+ public long InitialDelayMs { get; set; }
+
+ public long IntervalMs { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
index be0b3ddc3f..6f7ebf1565 100644
--- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
+++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Model.Net;
namespace MediaBrowser.Controller.Net
diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs
index 08d9bc12a2..7caba1097a 100644
--- a/MediaBrowser.Controller/Notifications/INotificationManager.cs
+++ b/MediaBrowser.Controller/Notifications/INotificationManager.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Controller.Notifications
/// <returns>Task.</returns>
Task SendNotification(NotificationRequest request, CancellationToken cancellationToken);
- Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken);
+ Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken);
/// <summary>
/// Adds the parts.
diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs
index fa947220ad..535c08795b 100644
--- a/MediaBrowser.Controller/Notifications/INotificationService.cs
+++ b/MediaBrowser.Controller/Notifications/INotificationService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Threading;
diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs
index d768abfe73..4be0e09ae7 100644
--- a/MediaBrowser.Controller/Notifications/UserNotification.cs
+++ b/MediaBrowser.Controller/Notifications/UserNotification.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index ebc37bd1f3..0a9073e7f5 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -47,21 +49,23 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Gets chapters for an item.
/// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
+ /// <param name="id">The item.</param>
+ /// <returns>The list of chapter info.</returns>
List<ChapterInfo> GetChapters(BaseItem id);
/// <summary>
/// Gets a single chapter for an item.
/// </summary>
- /// <param name="id"></param>
- /// <param name="index"></param>
- /// <returns></returns>
+ /// <param name="id">The item.</param>
+ /// <param name="index">The chapter index.</param>
+ /// <returns>The chapter info at the specified index.</returns>
ChapterInfo GetChapter(BaseItem id, int index);
/// <summary>
/// Saves the chapters.
/// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="chapters">The list of chapters to save.</param>
void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters);
/// <summary>
@@ -100,6 +104,7 @@ namespace MediaBrowser.Controller.Persistence
/// <param name="query">The query.</param>
/// <returns>IEnumerable&lt;Guid&gt;.</returns>
QueryResult<Guid> GetItemIds(InternalItemsQuery query);
+
/// <summary>
/// Gets the items.
/// </summary>
@@ -152,8 +157,7 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Updates the inherited values.
/// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- void UpdateInheritedValues(CancellationToken cancellationToken);
+ void UpdateInheritedValues();
int GetCount(InternalItemsQuery query);
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
index 81ba513cef..c43acfb6de 100644
--- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Controller.Entities;
@@ -16,7 +18,6 @@ namespace MediaBrowser.Controller.Persistence
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken);
/// <summary>
@@ -38,17 +39,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/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index fbf2c52131..f6c5920709 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="itemIds">The item ids.</param>
/// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns>
- Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId);
+ Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary>
/// Removes from playlist.
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 216dd27098..5e671a725d 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -1,8 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
@@ -13,39 +16,33 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Playlists
{
public class Playlist : Folder, IHasShares
{
- public static string[] SupportedExtensions =
- {
- ".m3u",
- ".m3u8",
- ".pls",
- ".wpl",
- ".zpl"
- };
-
- public Guid OwnerUserId { get; set; }
-
- public Share[] Shares { get; set; }
+ public static readonly IReadOnlyList<string> SupportedExtensions = new[]
+ {
+ ".m3u",
+ ".m3u8",
+ ".pls",
+ ".wpl",
+ ".zpl"
+ };
public Playlist()
{
Shares = Array.Empty<Share>();
}
+ public Guid OwnerUserId { get; set; }
+
+ public Share[] Shares { get; set; }
+
[JsonIgnore]
public bool IsFile => IsPlaylistFile(Path);
- public static bool IsPlaylistFile(string path)
- {
- return System.IO.Path.HasExtension(path);
- }
-
[JsonIgnore]
public override string ContainingFolderPath
{
@@ -77,6 +74,41 @@ namespace MediaBrowser.Controller.Playlists
[JsonIgnore]
public override bool SupportsCumulativeRunTimeTicks => true;
+ [JsonIgnore]
+ public override bool IsPreSorted => true;
+
+ public string PlaylistMediaType { get; set; }
+
+ [JsonIgnore]
+ public override string MediaType => PlaylistMediaType;
+
+ [JsonIgnore]
+ private bool IsSharedItem
+ {
+ get
+ {
+ var path = Path;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return false;
+ }
+
+ return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path);
+ }
+ }
+
+ public static bool IsPlaylistFile(string path)
+ {
+ // The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot).
+ return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
+ }
+
+ public void SetMediaType(string value)
+ {
+ PlaylistMediaType = value;
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
return 1;
@@ -98,7 +130,7 @@ namespace MediaBrowser.Controller.Playlists
return new List<BaseItem>();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
@@ -125,10 +157,7 @@ namespace MediaBrowser.Controller.Playlists
private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query)
{
- if (query == null)
- {
- query = new InternalItemsQuery(user);
- }
+ query ??= new InternalItemsQuery(user);
query.IsFolder = false;
@@ -160,9 +189,9 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = new[] { musicGenre.Id },
- OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
});
}
@@ -172,9 +201,9 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { typeof(Audio).Name },
+ IncludeItemTypes = new[] { nameof(Audio) },
ArtistIds = new[] { musicArtist.Id },
- OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
});
}
@@ -197,35 +226,6 @@ namespace MediaBrowser.Controller.Playlists
return new[] { item };
}
- [JsonIgnore]
- public override bool IsPreSorted => true;
-
- public string PlaylistMediaType { get; set; }
-
- [JsonIgnore]
- public override string MediaType => PlaylistMediaType;
-
- public void SetMediaType(string value)
- {
- PlaylistMediaType = value;
- }
-
- [JsonIgnore]
- private bool IsSharedItem
- {
- get
- {
- var path = Path;
-
- if (string.IsNullOrEmpty(path))
- {
- return false;
- }
-
- return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path);
- }
- }
-
public override bool IsVisible(User user)
{
if (!IsSharedItem)
diff --git a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs
deleted file mode 100644
index bf15fe0407..0000000000
--- a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma warning disable CS1591
-
-using System.IO;
-using System.Reflection;
-
-namespace MediaBrowser.Controller.Plugins
-{
- public interface ILocalizablePlugin
- {
- Stream GetDictionary(string culture);
- }
-
- public static class LocalizablePluginHelper
- {
- public static Stream GetDictionary(Assembly assembly, string manifestPrefix, string culture)
- {
- // Find all dictionaries using GetManifestResourceNames, start start with the prefix
- // Return the one for the culture if exists, otherwise return the default
- return null;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs
deleted file mode 100644
index 93eab42cc7..0000000000
--- a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System.IO;
-using MediaBrowser.Common.Plugins;
-
-namespace MediaBrowser.Controller.Plugins
-{
- /// <summary>
- /// Interface IConfigurationPage.
- /// </summary>
- public interface IPluginConfigurationPage
- {
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- string Name { get; }
-
- /// <summary>
- /// Gets the type of the configuration page.
- /// </summary>
- /// <value>The type of the configuration page.</value>
- ConfigurationPageType ConfigurationPageType { get; }
-
- /// <summary>
- /// Gets the plugin.
- /// </summary>
- /// <value>The plugin.</value>
- IPlugin Plugin { get; }
-
- /// <summary>
- /// Gets the HTML stream.
- /// </summary>
- /// <returns>Stream.</returns>
- Stream GetHtmlStream();
- }
-
- /// <summary>
- /// Enum ConfigurationPageType.
- /// </summary>
- public enum ConfigurationPageType
- {
- /// <summary>
- /// The plugin configuration.
- /// </summary>
- PluginConfiguration,
-
- /// <summary>
- /// The none.
- /// </summary>
- None
- }
-}
diff --git a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
new file mode 100644
index 0000000000..2b831103a5
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Plugins
+{
+ /// <summary>
+ /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task.
+ /// </summary>
+ public interface IRunBeforeStartup
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
index b44e2531e1..6024661e15 100644
--- a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
+++ b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
@@ -14,13 +14,7 @@ namespace MediaBrowser.Controller.Plugins
/// <summary>
/// Run the initialization for this module. This method is invoked at application start.
/// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task RunAsync();
}
-
- /// <summary>
- /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task.
- /// </summary>
- public interface IRunBeforeStartup
- {
- }
}
diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs
index 276bcf1252..aefa520e72 100644
--- a/MediaBrowser.Controller/Providers/AlbumInfo.cs
+++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
@@ -7,6 +7,13 @@ namespace MediaBrowser.Controller.Providers
{
public class AlbumInfo : ItemLookupInfo
{
+ public AlbumInfo()
+ {
+ ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SongInfos = new List<SongInfo>();
+ AlbumArtists = Array.Empty<string>();
+ }
+
/// <summary>
/// Gets or sets the album artist.
/// </summary>
@@ -20,12 +27,5 @@ namespace MediaBrowser.Controller.Providers
public Dictionary<string, string> ArtistProviderIds { get; set; }
public List<SongInfo> SongInfos { get; set; }
-
- public AlbumInfo()
- {
- ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- SongInfos = new List<SongInfo>();
- AlbumArtists = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/Providers/ArtistInfo.cs b/MediaBrowser.Controller/Providers/ArtistInfo.cs
index adf885baa6..4854d1a5fa 100644
--- a/MediaBrowser.Controller/Providers/ArtistInfo.cs
+++ b/MediaBrowser.Controller/Providers/ArtistInfo.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System.Collections.Generic;
@@ -6,11 +6,11 @@ namespace MediaBrowser.Controller.Providers
{
public class ArtistInfo : ItemLookupInfo
{
- public List<SongInfo> SongInfos { get; set; }
-
public ArtistInfo()
{
SongInfos = new List<SongInfo>();
}
+
+ public List<SongInfo> SongInfos { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Providers/BookInfo.cs b/MediaBrowser.Controller/Providers/BookInfo.cs
index cce0a25fcb..3055c5d871 100644
--- a/MediaBrowser.Controller/Providers/BookInfo.cs
+++ b/MediaBrowser.Controller/Providers/BookInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Providers
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index f77455485a..b312702704 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.IO;
@@ -11,11 +12,11 @@ namespace MediaBrowser.Controller.Providers
{
private readonly IFileSystem _fileSystem;
- private readonly Dictionary<string, FileSystemMetadata[]> _cache = new Dictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new (StringComparer.Ordinal);
- private readonly Dictionary<string, FileSystemMetadata> _fileCache = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new (StringComparer.Ordinal);
- private readonly Dictionary<string, List<string>> _filePathCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new (StringComparer.Ordinal);
public DirectoryService(IFileSystem fileSystem)
{
@@ -24,22 +25,16 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
- if (!_cache.TryGetValue(path, out FileSystemMetadata[] entries))
- {
- entries = _fileSystem.GetFileSystemEntries(path).ToArray();
-
- _cache[path] = entries;
- }
-
- return entries;
+ 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);
@@ -49,38 +44,39 @@ namespace MediaBrowser.Controller.Providers
return list;
}
- public FileSystemMetadata GetFile(string path)
+ public FileSystemMetadata? GetFile(string path)
{
- if (!_fileCache.TryGetValue(path, out FileSystemMetadata file))
+ if (!_fileCache.TryGetValue(path, out var result))
{
- file = _fileSystem.GetFileInfo(path);
-
- if (file != null && file.Exists)
- {
- _fileCache[path] = file;
- }
- else
+ var file = _fileSystem.GetFileInfo(path);
+ if (file.Exists)
{
- return null;
+ result = file;
+ _fileCache.TryAdd(path, result);
}
}
- return file;
+ return result;
}
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.TryGetValue(path, out List<string> result))
+ if (clearCache)
{
- result = _fileSystem.GetFilePaths(path).ToList();
+ _filePathCache.TryRemove(path, out _);
+ }
- _filePathCache[path] = result;
+ var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem);
+
+ if (sort)
+ {
+ filePaths.Sort();
}
- return result;
+ return filePaths;
}
}
}
diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs
index 006174be8c..66fddb0e38 100644
--- a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs
+++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
index a4c8dab7ea..b59a037384 100644
--- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs
+++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA2227, CS1591
using System;
using System.Collections.Generic;
@@ -7,6 +9,11 @@ namespace MediaBrowser.Controller.Providers
{
public class EpisodeInfo : ItemLookupInfo
{
+ public EpisodeInfo()
+ {
+ SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
public Dictionary<string, string> SeriesProviderIds { get; set; }
public int? IndexNumberEnd { get; set; }
@@ -14,10 +21,5 @@ namespace MediaBrowser.Controller.Providers
public bool IsMissingEpisode { get; set; }
public string SeriesDisplayOrder { get; set; }
-
- public EpisodeInfo()
- {
- SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index f06481c7a9..48d6276918 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA1819, CS1591
using System.Collections.Generic;
using MediaBrowser.Model.IO;
@@ -11,10 +11,10 @@ namespace MediaBrowser.Controller.Providers
List<FileSystemMetadata> GetFiles(string path);
- FileSystemMetadata GetFile(string path);
+ FileSystemMetadata? GetFile(string path);
IReadOnlyList<string> GetFilePaths(string path);
- IReadOnlyList<string> GetFilePaths(string path, bool clearCache);
+ IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
}
}
diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs
index 5e38446bc9..e2dbef2bc1 100644
--- a/MediaBrowser.Controller/Providers/IExternalId.cs
+++ b/MediaBrowser.Controller/Providers/IExternalId.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs
index c129eddb3a..f78bd6ddf4 100644
--- a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs
+++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs
@@ -10,6 +10,6 @@ namespace MediaBrowser.Controller.Providers
/// </summary>
public interface ILocalImageProvider : IImageProvider
{
- List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService);
+ IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService);
}
}
diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs
index 5f3d4274ef..05fbb18ee4 100644
--- a/MediaBrowser.Controller/Providers/IMetadataService.cs
+++ b/MediaBrowser.Controller/Providers/IMetadataService.cs
@@ -11,6 +11,12 @@ namespace MediaBrowser.Controller.Providers
public interface IMetadataService
{
/// <summary>
+ /// Gets the order.
+ /// </summary>
+ /// <value>The order.</value>
+ int Order { get; }
+
+ /// <summary>
/// Determines whether this instance can refresh the specified item.
/// </summary>
/// <param name="item">The item.</param>
@@ -27,11 +33,5 @@ namespace MediaBrowser.Controller.Providers
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken);
-
- /// <summary>
- /// Gets the order.
- /// </summary>
- /// <value>The order.</value>
- int Order { get; }
}
}
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 996ec27c09..9f7a76be64 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -6,9 +8,7 @@ using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
@@ -22,9 +22,18 @@ namespace MediaBrowser.Controller.Providers
/// </summary>
public interface IProviderManager
{
+ event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
+
+ event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
+
+ event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
+
/// <summary>
/// Queues the refresh.
/// </summary>
+ /// <param name="itemId">Item ID.</param>
+ /// <param name="options">MetadataRefreshOptions for operation.</param>
+ /// <param name="priority">RefreshPriority for operation.</param>
void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority);
/// <summary>
@@ -46,6 +55,14 @@ namespace MediaBrowser.Controller.Providers
Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken);
/// <summary>
+ /// Runs multiple metadata refreshes concurrently.
+ /// </summary>
+ /// <param name="action">The action to run.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
+ Task RunMetadataRefresh(Func<Task> action, CancellationToken cancellationToken);
+
+ /// <summary>
/// Saves the image.
/// </summary>
/// <param name="item">The item.</param>
@@ -71,6 +88,13 @@ namespace MediaBrowser.Controller.Providers
/// <summary>
/// Saves the image.
/// </summary>
+ /// <param name="item">Image to save.</param>
+ /// <param name="source">Source of image.</param>
+ /// <param name="mimeType">Mime type image.</param>
+ /// <param name="type">Type of image.</param>
+ /// <param name="imageIndex">Index of image.</param>
+ /// <param name="saveLocallyWithMedia">Option to save locally.</param>
+ /// <param name="cancellationToken">CancellationToken to use with operation.</param>
/// <returns>Task.</returns>
Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken);
@@ -79,8 +103,16 @@ namespace MediaBrowser.Controller.Providers
/// <summary>
/// Adds the metadata providers.
/// </summary>
- void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders,
- IEnumerable<IMetadataSaver> savers,
+ /// <param name="imageProviders">Image providers to use.</param>
+ /// <param name="metadataServices">Metadata services to use.</param>
+ /// <param name="metadataProviders">Metadata providers to use.</param>
+ /// <param name="metadataSavers">Metadata savers to use.</param>
+ /// <param name="externalIds">External IDs to use.</param>
+ void AddParts(
+ IEnumerable<IImageProvider> imageProviders,
+ IEnumerable<IMetadataService> metadataServices,
+ IEnumerable<IMetadataProvider> metadataProviders,
+ IEnumerable<IMetadataSaver> metadataSavers,
IEnumerable<IExternalId> externalIds);
/// <summary>
@@ -124,12 +156,14 @@ namespace MediaBrowser.Controller.Providers
/// </summary>
/// <param name="item">The item.</param>
/// <param name="updateType">Type of the update.</param>
- /// <returns>Task.</returns>
void SaveMetadata(BaseItem item, ItemUpdateType updateType);
/// <summary>
/// Saves the metadata.
/// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="updateType">Type of the update.</param>
+ /// <param name="savers">The metadata savers.</param>
void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers);
/// <summary>
@@ -171,18 +205,5 @@ namespace MediaBrowser.Controller.Providers
void OnRefreshComplete(BaseItem item);
double? GetRefreshProgress(Guid id);
-
- event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
-
- event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
-
- event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
- }
-
- public enum RefreshPriority
- {
- High = 0,
- Normal = 1,
- Low = 2
}
}
diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
index ee8f5b860a..de1631dcf4 100644
--- a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
+++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
@@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
diff --git a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
index 9592baa7c1..e401ed211c 100644
--- a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
+++ b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
@@ -3,7 +3,6 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
namespace MediaBrowser.Controller.Providers
{
diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
index 9fc379f045..2ac4c728ba 100644
--- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1819, CS1591
using System;
using System.Linq;
@@ -8,6 +10,15 @@ namespace MediaBrowser.Controller.Providers
{
public class ImageRefreshOptions
{
+ public ImageRefreshOptions(IDirectoryService directoryService)
+ {
+ ImageRefreshMode = MetadataRefreshMode.Default;
+ DirectoryService = directoryService;
+
+ ReplaceImages = Array.Empty<ImageType>();
+ IsAutomated = true;
+ }
+
public MetadataRefreshMode ImageRefreshMode { get; set; }
public IDirectoryService DirectoryService { get; private set; }
@@ -18,15 +29,6 @@ namespace MediaBrowser.Controller.Providers
public bool IsAutomated { get; set; }
- public ImageRefreshOptions(IDirectoryService directoryService)
- {
- ImageRefreshMode = MetadataRefreshMode.Default;
- DirectoryService = directoryService;
-
- ReplaceImages = Array.Empty<ImageType>();
- IsAutomated = true;
- }
-
public bool IsReplacingImage(ImageType type)
{
return ImageRefreshMode == MetadataRefreshMode.FullRefresh &&
diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs
index b50def043f..b8dd416a2d 100644
--- a/MediaBrowser.Controller/Providers/ItemInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -14,8 +16,7 @@ namespace MediaBrowser.Controller.Providers
ContainingFolderPath = item.ContainingFolderPath;
IsInMixedFolder = item.IsInMixedFolder;
- var video = item as Video;
- if (video != null)
+ if (item is Video video)
{
VideoType = video.VideoType;
IsPlaceHolder = video.IsPlaceHolder;
diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
index 49974c2a37..460f4e500f 100644
--- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA2227, CS1591
using System;
using System.Collections.Generic;
@@ -8,6 +10,12 @@ namespace MediaBrowser.Controller.Providers
{
public class ItemLookupInfo : IHasProviderIds
{
+ public ItemLookupInfo()
+ {
+ IsAutomated = true;
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
/// <summary>
/// Gets or sets the name.
/// </summary>
@@ -15,6 +23,18 @@ 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>
+ public string Path { get; set; }
+
+ /// <summary>
/// Gets or sets the metadata language.
/// </summary>
/// <value>The metadata language.</value>
@@ -45,11 +65,5 @@ namespace MediaBrowser.Controller.Providers
public DateTime? PremiereDate { get; set; }
public bool IsAutomated { get; set; }
-
- public ItemLookupInfo()
- {
- IsAutomated = true;
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs
index 41801862fd..a8e70e6d08 100644
--- a/MediaBrowser.Controller/Providers/LocalImageInfo.cs
+++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index b92b837012..a42c7f8b5e 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1819, CS1591
using System;
using System.Linq;
@@ -9,21 +11,6 @@ namespace MediaBrowser.Controller.Providers
{
public class MetadataRefreshOptions : ImageRefreshOptions
{
- /// <summary>
- /// When paired with MetadataRefreshMode=FullRefresh, all existing data will be overwritten with new data from the providers.
- /// </summary>
- public bool ReplaceAllMetadata { get; set; }
-
- public MetadataRefreshMode MetadataRefreshMode { get; set; }
-
- public RemoteSearchResult SearchResult { get; set; }
-
- public string[] RefreshPaths { get; set; }
-
- public bool ForceSave { get; set; }
-
- public bool EnableRemoteContentProbe { get; set; }
-
public MetadataRefreshOptions(IDirectoryService directoryService)
: base(directoryService)
{
@@ -45,15 +32,28 @@ namespace MediaBrowser.Controller.Providers
if (copy.RefreshPaths != null && copy.RefreshPaths.Length > 0)
{
- if (RefreshPaths == null)
- {
- RefreshPaths = Array.Empty<string>();
- }
+ RefreshPaths ??= Array.Empty<string>();
RefreshPaths = copy.RefreshPaths.ToArray();
}
}
+ /// <summary>
+ /// Gets or sets a value indicating whether all existing data should be overwritten with new data from providers
+ /// when paired with MetadataRefreshMode=FullRefresh.
+ /// </summary>
+ public bool ReplaceAllMetadata { get; set; }
+
+ public MetadataRefreshMode MetadataRefreshMode { get; set; }
+
+ public RemoteSearchResult SearchResult { get; set; }
+
+ public string[] RefreshPaths { get; set; }
+
+ public bool ForceSave { get; set; }
+
+ public bool EnableRemoteContentProbe { get; set; }
+
public bool RefreshItem(BaseItem item)
{
if (RefreshPaths != null && RefreshPaths.Length > 0)
diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs
index 1c695cafa0..2085ae4adf 100644
--- a/MediaBrowser.Controller/Providers/MetadataResult.cs
+++ b/MediaBrowser.Controller/Providers/MetadataResult.cs
@@ -1,24 +1,40 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Providers
{
public class MetadataResult<T>
{
- public List<LocalImageInfo> Images { get; set; }
-
- public List<UserItemData> UserDataList { get; set; }
+ // Images aren't always used so the allocation is a waste a lot of the time
+ private List<LocalImageInfo> _images;
+ private List<(string url, ImageType type)> _remoteImages;
public MetadataResult()
{
- Images = new List<LocalImageInfo>();
ResultLanguage = "en";
}
+ public List<LocalImageInfo> Images
+ {
+ get => _images ??= new List<LocalImageInfo>();
+ set => _images = value;
+ }
+
+ public List<(string url, ImageType type)> RemoteImages
+ {
+ get => _remoteImages ??= new List<(string url, ImageType type)>();
+ set => _remoteImages = value;
+ }
+
+ public List<UserItemData> UserDataList { get; set; }
+
public List<PersonInfo> People { get; set; }
public bool HasMetadata { get; set; }
@@ -33,10 +49,7 @@ namespace MediaBrowser.Controller.Providers
public void AddPerson(PersonInfo p)
{
- if (People == null)
- {
- People = new List<PersonInfo>();
- }
+ People ??= new List<PersonInfo>();
PeopleHelper.AddPerson(People, p);
}
@@ -50,16 +63,15 @@ namespace MediaBrowser.Controller.Providers
{
People = new List<PersonInfo>();
}
-
- People.Clear();
+ else
+ {
+ People.Clear();
+ }
}
public UserItemData GetOrAddUserData(string userId)
{
- if (UserDataList == null)
- {
- UserDataList = new List<UserItemData>();
- }
+ UserDataList ??= new List<UserItemData>();
UserItemData userData = null;
diff --git a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs
index 0b927f6eb0..322320abdf 100644
--- a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs
+++ b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Providers/RefreshPriority.cs b/MediaBrowser.Controller/Providers/RefreshPriority.cs
new file mode 100644
index 0000000000..3619f679d6
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/RefreshPriority.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provider refresh priority.
+ /// </summary>
+ public enum RefreshPriority
+ {
+ /// <summary>
+ /// High priority.
+ /// </summary>
+ High = 0,
+
+ /// <summary>
+ /// Normal priority.
+ /// </summary>
+ Normal = 1,
+
+ /// <summary>
+ /// Low priority.
+ /// </summary>
+ Low = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs
index 9653bc1c4a..d4df5fa0db 100644
--- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs
+++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -12,7 +14,7 @@ namespace MediaBrowser.Controller.Providers
public Guid ItemId { get; set; }
/// <summary>
- /// Will only search within the given provider when set.
+ /// Gets or sets the provider name to search within if set.
/// </summary>
public string SearchProviderName { get; set; }
diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs
index 2a4c1f03c7..1edceb0e4a 100644
--- a/MediaBrowser.Controller/Providers/SeasonInfo.cs
+++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA2227, CS1591
using System;
using System.Collections.Generic;
@@ -7,11 +7,11 @@ namespace MediaBrowser.Controller.Providers
{
public class SeasonInfo : ItemLookupInfo
{
- public Dictionary<string, string> SeriesProviderIds { get; set; }
-
public SeasonInfo()
{
SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
+
+ public Dictionary<string, string> SeriesProviderIds { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs
index 58f76dca91..4b64a8a982 100644
--- a/MediaBrowser.Controller/Providers/SongInfo.cs
+++ b/MediaBrowser.Controller/Providers/SongInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,16 +9,16 @@ namespace MediaBrowser.Controller.Providers
{
public class SongInfo : ItemLookupInfo
{
- public IReadOnlyList<string> AlbumArtists { get; set; }
-
- public string Album { get; set; }
-
- public IReadOnlyList<string> Artists { get; set; }
-
public SongInfo()
{
Artists = Array.Empty<string>();
AlbumArtists = Array.Empty<string>();
}
+
+ public IReadOnlyList<string> AlbumArtists { get; set; }
+
+ public string Album { get; set; }
+
+ public IReadOnlyList<string> Artists { get; set; }
}
}
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
new file mode 100644
index 0000000000..ad34c86042
--- /dev/null
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -0,0 +1,37 @@
+using System;
+using MediaBrowser.Model.QuickConnect;
+
+namespace MediaBrowser.Controller.QuickConnect
+{
+ /// <summary>
+ /// Quick connect standard interface.
+ /// </summary>
+ public interface IQuickConnect
+ {
+ /// <summary>
+ /// Gets a value indicating whether quick connect is enabled or not.
+ /// </summary>
+ bool IsEnabled { get; }
+
+ /// <summary>
+ /// Initiates a new quick connect request.
+ /// </summary>
+ /// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns>
+ QuickConnectResult TryConnect();
+
+ /// <summary>
+ /// Checks the status of an individual request.
+ /// </summary>
+ /// <param name="secret">Unique secret identifier of the request.</param>
+ /// <returns>Quick connect result.</returns>
+ QuickConnectResult CheckRequestStatus(string secret);
+
+ /// <summary>
+ /// Authorizes a quick connect request to connect as the calling user.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="code">Identifying code for the request.</param>
+ /// <returns>A boolean indicating if the authorization completed successfully.</returns>
+ bool AuthorizeRequest(Guid userId, string code);
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index b99c468435..b95d00aa3c 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -14,21 +14,23 @@ namespace MediaBrowser.Controller.Resolvers
public interface IItemResolver
{
/// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ ResolverPriority Priority { get; }
+
+ /// <summary>
/// Resolves the path.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
BaseItem ResolvePath(ItemResolveArgs args);
- /// <summary>
- /// Gets the priority.
- /// </summary>
- /// <value>The priority.</value>
- ResolverPriority Priority { get; }
}
public interface IMultiItemResolver
{
- MultiItemResolverResult ResolveMultiple(Folder parent,
+ MultiItemResolverResult ResolveMultiple(
+ Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService);
@@ -36,14 +38,14 @@ namespace MediaBrowser.Controller.Resolvers
public class MultiItemResolverResult
{
- public List<BaseItem> Items { get; set; }
-
- public List<FileSystemMetadata> ExtraFiles { get; set; }
-
public MultiItemResolverResult()
{
Items = new List<BaseItem>();
ExtraFiles = new List<FileSystemMetadata>();
}
+
+ public List<BaseItem> Items { get; set; }
+
+ public List<FileSystemMetadata> ExtraFiles { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
index bb80e60256..a07b3e8988 100644
--- a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
+++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
@@ -4,7 +4,7 @@ using MediaBrowser.Model.IO;
namespace MediaBrowser.Controller.Resolvers
{
/// <summary>
- /// Provides a base "rule" that anyone can use to have paths ignored by the resolver
+ /// Provides a base "rule" that anyone can use to have paths ignored by the resolver.
/// </summary>
public interface IResolverIgnoreRule
{
diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index 67acdd9a3c..7fd54fcc69 100644
--- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -6,27 +8,27 @@ namespace MediaBrowser.Controller.Resolvers
/// <summary>
/// Class ItemResolver.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of BaseItem.</typeparam>
public abstract class ItemResolver<T> : IItemResolver
where T : BaseItem, new()
{
/// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public virtual ResolverPriority Priority => ResolverPriority.First;
+
+ /// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- protected virtual T Resolve(ItemResolveArgs args)
+ public virtual T Resolve(ItemResolveArgs args)
{
return null;
}
/// <summary>
- /// Gets the priority.
- /// </summary>
- /// <value>The priority.</value>
- public virtual ResolverPriority Priority => ResolverPriority.First;
-
- /// <summary>
/// Sets initial values on the newly resolved item.
/// </summary>
/// <param name="item">The item.</param>
diff --git a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs
index ac73a5ea89..d4f975b6d1 100644
--- a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs
+++ b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs
@@ -26,8 +26,13 @@ namespace MediaBrowser.Controller.Resolvers
Fourth = 4,
/// <summary>
+ /// The Fifth.
+ /// </summary>
+ Fifth = 5,
+
+ /// <summary>
/// The last.
/// </summary>
- Last = 5
+ Last = 6
}
}
diff --git a/MediaBrowser.Controller/Security/AuthenticationInfo.cs b/MediaBrowser.Controller/Security/AuthenticationInfo.cs
index efac9273ec..b4b242f1b2 100644
--- a/MediaBrowser.Controller/Security/AuthenticationInfo.cs
+++ b/MediaBrowser.Controller/Security/AuthenticationInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
index c5f3da0b1b..3af6a525c7 100644
--- a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
+++ b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
index 883b74165c..bd1289c1a6 100644
--- a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
+++ b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using MediaBrowser.Model.Devices;
@@ -11,14 +13,12 @@ namespace MediaBrowser.Controller.Security
/// Creates the specified information.
/// </summary>
/// <param name="info">The information.</param>
- /// <returns>Task.</returns>
void Create(AuthenticationInfo info);
/// <summary>
/// Updates the specified information.
/// </summary>
/// <param name="info">The information.</param>
- /// <returns>Task.</returns>
void Update(AuthenticationInfo info);
/// <summary>
diff --git a/MediaBrowser.Controller/Session/AuthenticationRequest.cs b/MediaBrowser.Controller/Session/AuthenticationRequest.cs
index cc321fd22e..647c75e66e 100644
--- a/MediaBrowser.Controller/Session/AuthenticationRequest.cs
+++ b/MediaBrowser.Controller/Session/AuthenticationRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -12,6 +14,7 @@ namespace MediaBrowser.Controller.Session
public string Password { get; set; }
+ [Obsolete("Send full password in Password field")]
public string PasswordSha1 { get; set; }
public string App { get; set; }
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index 22d6e2a04e..b38ee11462 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -1,8 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Session
{
@@ -23,6 +26,12 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message.
/// </summary>
- Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
+ /// <typeparam name="T">The type of data.</typeparam>
+ /// <param name="name">Name of message type.</param>
+ /// <param name="messageId">Message ID.</param>
+ /// <param name="data">Data to send.</param>
+ /// <param name="cancellationToken">CancellationToken for operation.</param>
+ /// <returns>A task.</returns>
+ Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 9237d21dfa..0ff32fb536 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -47,6 +49,11 @@ namespace MediaBrowser.Controller.Session
event EventHandler<SessionEventArgs> SessionActivity;
/// <summary>
+ /// Occurs when [session controller connected].
+ /// </summary>
+ event EventHandler<SessionEventArgs> SessionControllerConnected;
+
+ /// <summary>
/// Occurs when [capabilities changed].
/// </summary>
event EventHandler<SessionEventArgs> CapabilitiesChanged;
@@ -76,8 +83,15 @@ namespace MediaBrowser.Controller.Session
/// <param name="deviceName">Name of the device.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
+ /// <returns>Session information.</returns>
SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
+ /// <summary>
+ /// Used to report that a session controller has connected.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ void OnSessionControllerConnected(SessionInfo session);
+
void UpdateDeviceName(string sessionId, string reportedDeviceName);
/// <summary>
@@ -92,7 +106,7 @@ namespace MediaBrowser.Controller.Session
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if an argument is null.</exception>
Task OnPlaybackProgress(PlaybackProgressInfo info);
Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated);
@@ -102,14 +116,13 @@ namespace MediaBrowser.Controller.Session
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if an argument is null.</exception>
Task OnPlaybackStopped(PlaybackStopInfo info);
/// <summary>
/// Reports the session ended.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
- /// <returns>Task.</returns>
void ReportSessionEnded(string sessionId);
/// <summary>
@@ -143,22 +156,23 @@ namespace MediaBrowser.Controller.Session
Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken);
/// <summary>
- /// Sends the SyncPlayCommand.
+ /// Sends a SyncPlayCommand to a session.
/// </summary>
- /// <param name="sessionId">The session id.</param>
+ /// <param name="session">The session.</param>
/// <param name="command">The command.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken);
+ Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken);
/// <summary>
- /// Sends the SyncPlayGroupUpdate.
+ /// Sends a SyncPlayGroupUpdate to a session.
/// </summary>
- /// <param name="sessionId">The session id.</param>
+ /// <param name="session">The session.</param>
/// <param name="command">The group update.</param>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <typeparam name="T">Type of group.</typeparam>
/// <returns>Task.</returns>
- Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken);
+ Task SendSyncPlayGroupUpdate<T>(SessionInfo session, GroupUpdate<T> command, CancellationToken cancellationToken);
/// <summary>
/// Sends the browse command.
@@ -183,32 +197,45 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message to admin sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="name">The name.</param>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="name">Message type name.</param>
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken);
/// <summary>
/// Sends the message to user sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="userIds">Users to send messages to.</param>
+ /// <param name="name">Message type name.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken);
- Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken);
+ /// <summary>
+ /// Sends the message to user sessions.
+ /// </summary>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="userIds">Users to send messages to.</param>
+ /// <param name="name">Message type name.</param>
+ /// <param name="dataFn">Data function.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken);
/// <summary>
/// Sends the message to user device sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">Type of data.</typeparam>
/// <param name="deviceId">The device identifier.</param>
- /// <param name="name">The name.</param>
+ /// <param name="name">Message type name.</param>
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken);
/// <summary>
/// Sends the restart required message.
@@ -267,6 +294,14 @@ namespace MediaBrowser.Controller.Session
Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request);
/// <summary>
+ /// Authenticates a new session with quick connect.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="token">Quick connect access token.</param>
+ /// <returns>Task{SessionInfo}.</returns>
+ Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token);
+
+ /// <summary>
/// Creates the new session.
/// </summary>
/// <param name="request">The request.</param>
@@ -325,21 +360,21 @@ namespace MediaBrowser.Controller.Session
/// Logouts the specified access token.
/// </summary>
/// <param name="accessToken">The access token.</param>
- /// <returns>Task.</returns>
void Logout(string accessToken);
+
void Logout(AuthenticationInfo accessToken);
/// <summary>
/// Revokes the user tokens.
/// </summary>
- /// <returns>Task.</returns>
+ /// <param name="userId">User ID.</param>
+ /// <param name="currentAccessToken">Current access token.</param>
void RevokeUserTokens(Guid userId, string currentAccessToken);
/// <summary>
/// Revokes the token.
/// </summary>
/// <param name="id">The identifier.</param>
- /// <returns>Task.</returns>
void RevokeToken(string id);
void CloseIfNeeded(SessionInfo session);
diff --git a/MediaBrowser.Controller/Session/SessionEventArgs.cs b/MediaBrowser.Controller/Session/SessionEventArgs.cs
index 097e32eaec..269fe7dc4d 100644
--- a/MediaBrowser.Controller/Session/SessionEventArgs.cs
+++ b/MediaBrowser.Controller/Session/SessionEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 054fd33d9d..6134c0cf33 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -1,6 +1,9 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
@@ -22,7 +25,6 @@ namespace MediaBrowser.Controller.Session
private readonly ISessionManager _sessionManager;
private readonly ILogger _logger;
-
private readonly object _progressLock = new object();
private Timer _progressTimer;
private PlaybackProgressInfo _lastProgressInfo;
@@ -52,10 +54,10 @@ namespace MediaBrowser.Controller.Session
public string RemoteEndPoint { get; set; }
/// <summary>
- /// Gets or sets the playable media types.
+ /// Gets the playable media types.
/// </summary>
/// <value>The playable media types.</value>
- public string[] PlayableMediaTypes
+ public IReadOnlyList<string> PlayableMediaTypes
{
get
{
@@ -228,11 +230,11 @@ namespace MediaBrowser.Controller.Session
public string UserPrimaryImageTag { get; set; }
/// <summary>
- /// Gets or sets the supported commands.
+ /// Gets the supported commands.
/// </summary>
/// <value>The supported commands.</value>
- public string[] SupportedCommands
- => Capabilities == null ? Array.Empty<string>() : Capabilities.SupportedCommands;
+ public IReadOnlyList<GeneralCommandType> SupportedCommands
+ => Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
{
diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
deleted file mode 100644
index 70cb9eebe0..0000000000
--- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-#pragma warning disable CS1591
-
-#nullable enable
-
-using System;
-using System.Collections.Generic;
-
-namespace MediaBrowser.Controller.Sorting
-{
- public class AlphanumComparator : IComparer<string?>
- {
- public static int CompareValues(string? s1, string? s2)
- {
- if (s1 == null && s2 == null)
- {
- return 0;
- }
- else if (s1 == null)
- {
- return -1;
- }
- else if (s2 == null)
- {
- return 1;
- }
-
- int len1 = s1.Length;
- int len2 = s2.Length;
-
- // Early return for empty strings
- if (len1 == 0 && len2 == 0)
- {
- return 0;
- }
- else if (len1 == 0)
- {
- return -1;
- }
- else if (len2 == 0)
- {
- return 1;
- }
-
- int pos1 = 0;
- int pos2 = 0;
-
- do
- {
- int start1 = pos1;
- int start2 = pos2;
-
- bool isNum1 = char.IsDigit(s1[pos1++]);
- bool isNum2 = char.IsDigit(s2[pos2++]);
-
- while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1)
- {
- pos1++;
- }
-
- while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2)
- {
- pos2++;
- }
-
- var span1 = s1.AsSpan(start1, pos1 - start1);
- var span2 = s2.AsSpan(start2, pos2 - start2);
-
- if (isNum1 && isNum2)
- {
- // Trim leading zeros so we can compare the length
- // of the strings to find the largest number
- span1 = span1.TrimStart('0');
- span2 = span2.TrimStart('0');
- var span1Len = span1.Length;
- var span2Len = span2.Length;
- if (span1Len < span2Len)
- {
- return -1;
- }
- else if (span1Len > span2Len)
- {
- return 1;
- }
- else if (span1Len >= 20) // Number is probably too big for a ulong
- {
- // Trim all the first digits that are the same
- int i = 0;
- while (i < span1Len && span1[i] == span2[i])
- {
- i++;
- }
-
- // If there are no more digits it's the same number
- if (i == span1Len)
- {
- continue;
- }
-
- // Only need to compare the most significant digit
- span1 = span1.Slice(i, 1);
- span2 = span2.Slice(i, 1);
- }
-
- if (!ulong.TryParse(span1, out var num1)
- || !ulong.TryParse(span2, out var num2))
- {
- return 0;
- }
- else if (num1 < num2)
- {
- return -1;
- }
- else if (num1 > num2)
- {
- return 1;
- }
- }
- else
- {
- int result = span1.CompareTo(span2, StringComparison.InvariantCulture);
- if (result != 0)
- {
- return result;
- }
- }
- } while (pos1 < len1 && pos2 < len2);
-
- return len1 - len2;
- }
-
- /// <inheritdoc />
- public int Compare(string? x, string? y)
- {
- return CompareValues(x, y);
- }
- }
-}
diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
index 727cbe639c..07fe1ea8a9 100644
--- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
@@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Sorting
/// <summary>
/// Interface IBaseItemComparer.
/// </summary>
- public interface IBaseItemComparer : IComparer<BaseItem>
+ public interface IBaseItemComparer : IComparer<BaseItem?>
{
/// <summary>
/// Gets the name.
diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
index 6d03d97ae3..bd47db39a6 100644
--- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Sorting
diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs
index 88467814cd..f9c0d39ddd 100644
--- a/MediaBrowser.Controller/Sorting/SortExtensions.cs
+++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs
@@ -3,12 +3,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Extensions;
namespace MediaBrowser.Controller.Sorting
{
public static class SortExtensions
{
- private static readonly AlphanumComparator _comparer = new AlphanumComparator();
+ private static readonly AlphanumericComparator _comparer = new AlphanumericComparator();
public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName)
{
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
index f43d523a63..3330dd5408 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -26,6 +28,11 @@ namespace MediaBrowser.Controller.Subtitles
/// <summary>
/// Searches the subtitles.
/// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="language">Subtitle language.</param>
+ /// <param name="isPerfectMatch">Require perfect match.</param>
+ /// <param name="cancellationToken">CancellationToken to use for the operation.</param>
+ /// <returns>Subtitles, wrapped in task.</returns>
Task<RemoteSubtitleInfo[]> SearchSubtitles(
Video video,
string language,
@@ -45,14 +52,31 @@ namespace MediaBrowser.Controller.Subtitles
/// <summary>
/// Downloads the subtitles.
/// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="subtitleId">Subtitle ID.</param>
+ /// <param name="cancellationToken">CancellationToken to use for the operation.</param>
+ /// <returns>A task.</returns>
Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken);
/// <summary>
/// Downloads the subtitles.
/// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="libraryOptions">Library options to use.</param>
+ /// <param name="subtitleId">Subtitle ID.</param>
+ /// <param name="cancellationToken">CancellationToken to use for the operation.</param>
+ /// <returns>A task.</returns>
Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken);
/// <summary>
+ /// Upload new subtitle.
+ /// </summary>
+ /// <param name="video">The video the subtitle belongs to.</param>
+ /// <param name="response">The subtitle response.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ Task UploadSubtitle(Video video, SubtitleResponse response);
+
+ /// <summary>
/// Gets the remote subtitles.
/// </summary>
/// <param name="id">The identifier.</param>
@@ -63,11 +87,16 @@ namespace MediaBrowser.Controller.Subtitles
/// <summary>
/// Deletes the subtitles.
/// </summary>
+ /// <param name="item">Media item.</param>
+ /// <param name="index">Subtitle index.</param>
+ /// <returns>A task.</returns>
Task DeleteSubtitles(BaseItem item, int index);
/// <summary>
/// Gets the providers.
/// </summary>
+ /// <param name="item">The media item.</param>
+ /// <returns>Subtitles providers.</returns>
SubtitleProviderInfo[] GetSupportedProviders(BaseItem item);
}
}
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
index a633262de9..326348d583 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs
index ce8141219a..c782f57961 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
index a86b057783..85b3e6fbd7 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.IO;
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
index 7d3c20e8f1..767d87d465 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -9,6 +11,15 @@ namespace MediaBrowser.Controller.Subtitles
{
public class SubtitleSearchRequest : IHasProviderIds
{
+ public SubtitleSearchRequest()
+ {
+ SearchAllProviders = true;
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ DisabledSubtitleFetchers = Array.Empty<string>();
+ SubtitleFetcherOrder = Array.Empty<string>();
+ }
+
public string Language { get; set; }
public string TwoLetterISOLanguageName { get; set; }
@@ -40,14 +51,5 @@ namespace MediaBrowser.Controller.Subtitles
public string[] DisabledSubtitleFetchers { get; set; }
public string[] SubtitleFetcherOrder { get; set; }
-
- public SubtitleSearchRequest()
- {
- SearchAllProviders = true;
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- DisabledSubtitleFetchers = Array.Empty<string>();
- SubtitleFetcherOrder = Array.Empty<string>();
- }
}
}
diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs
deleted file mode 100644
index e7395b136d..0000000000
--- a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Sync;
-
-namespace MediaBrowser.Controller.Sync
-{
- public interface IHasDynamicAccess
- {
- /// <summary>
- /// Gets the synced file information.
- /// </summary>
- /// <param name="id">The identifier.</param>
- /// <param name="target">The target.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task&lt;SyncedFileInfo&gt;.</returns>
- Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
deleted file mode 100644
index b2c53365c4..0000000000
--- a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace MediaBrowser.Controller.Sync
-{
- /// <summary>
- /// A marker interface.
- /// </summary>
- public interface IRemoteSyncProvider
- {
- }
-}
diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs
deleted file mode 100644
index c97fd70442..0000000000
--- a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Sync;
-
-namespace MediaBrowser.Controller.Sync
-{
- public interface IServerSyncProvider : ISyncProvider
- {
- /// <summary>
- /// Transfers the file.
- /// </summary>
- Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, Stream inputStream, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
-
- Task<QueryResult<FileSystemMetadata>> GetFiles(string[] directoryPathParts, SyncTarget target, CancellationToken cancellationToken);
- }
-
- public interface ISupportsDirectCopy
- {
- /// <summary>
- /// Sends the file.
- /// </summary>
- Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, string inputPath, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/Sync/ISyncProvider.cs b/MediaBrowser.Controller/Sync/ISyncProvider.cs
deleted file mode 100644
index 950cc73e85..0000000000
--- a/MediaBrowser.Controller/Sync/ISyncProvider.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.Sync;
-
-namespace MediaBrowser.Controller.Sync
-{
- public interface ISyncProvider
- {
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- string Name { get; }
-
- /// <summary>
- /// Gets the synchronize targets.
- /// </summary>
- /// <param name="userId">The user identifier.</param>
- /// <returns>IEnumerable&lt;SyncTarget&gt;.</returns>
- List<SyncTarget> GetSyncTargets(string userId);
-
- /// <summary>
- /// Gets all synchronize targets.
- /// </summary>
- /// <returns>IEnumerable&lt;SyncTarget&gt;.</returns>
- List<SyncTarget> GetAllSyncTargets();
- }
-}
diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs
deleted file mode 100644
index a626738fb2..0000000000
--- a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.Controller.Sync
-{
- public class SyncedFileInfo
- {
- public SyncedFileInfo()
- {
- RequiredHttpHeaders = new Dictionary<string, string>();
- }
-
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- public string Path { get; set; }
-
- public string[] PathParts { get; set; }
-
- /// <summary>
- /// Gets or sets the protocol.
- /// </summary>
- /// <value>The protocol.</value>
- public MediaProtocol Protocol { get; set; }
-
- /// <summary>
- /// Gets or sets the required HTTP headers.
- /// </summary>
- /// <value>The required HTTP headers.</value>
- public Dictionary<string, string> RequiredHttpHeaders { get; set; }
-
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
deleted file mode 100644
index e742df5179..0000000000
--- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Session;
-
-namespace MediaBrowser.Controller.SyncPlay
-{
- /// <summary>
- /// Class GroupInfo.
- /// </summary>
- /// <remarks>
- /// Class is not thread-safe, external locking is required when accessing methods.
- /// </remarks>
- public class GroupInfo
- {
- /// <summary>
- /// Gets the default ping value used for sessions.
- /// </summary>
- public long DefaultPing { get; } = 500;
-
- /// <summary>
- /// Gets or sets the group identifier.
- /// </summary>
- /// <value>The group identifier.</value>
- public Guid GroupId { get; } = Guid.NewGuid();
-
- /// <summary>
- /// Gets or sets the playing item.
- /// </summary>
- /// <value>The playing item.</value>
- public BaseItem PlayingItem { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether playback is paused.
- /// </summary>
- /// <value>Playback is paused.</value>
- public bool IsPaused { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether there are position ticks.
- /// </summary>
- /// <value>The position ticks.</value>
- public long PositionTicks { get; set; }
-
- /// <summary>
- /// Gets or sets the last activity.
- /// </summary>
- /// <value>The last activity.</value>
- public DateTime LastActivity { get; set; }
-
- /// <summary>
- /// Gets the participants.
- /// </summary>
- /// <value>The participants, or members of the group.</value>
- public Dictionary<string, GroupMember> Participants { get; } =
- new Dictionary<string, GroupMember>(StringComparer.OrdinalIgnoreCase);
-
- /// <summary>
- /// Checks if a session is in this group.
- /// </summary>
- /// <value><c>true</c> if the session is in this group; <c>false</c> otherwise.</value>
- public bool ContainsSession(string sessionId)
- {
- return Participants.ContainsKey(sessionId);
- }
-
- /// <summary>
- /// Adds the session to the group.
- /// </summary>
- /// <param name="session">The session.</param>
- public void AddSession(SessionInfo session)
- {
- if (ContainsSession(session.Id))
- {
- return;
- }
-
- var member = new GroupMember();
- member.Session = session;
- member.Ping = DefaultPing;
- member.IsBuffering = false;
- Participants[session.Id] = member;
- }
-
- /// <summary>
- /// Removes the session from the group.
- /// </summary>
- /// <param name="session">The session.</param>
- public void RemoveSession(SessionInfo session)
- {
- if (!ContainsSession(session.Id))
- {
- return;
- }
-
- Participants.Remove(session.Id, out _);
- }
-
- /// <summary>
- /// Updates the ping of a session.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="ping">The ping.</param>
- public void UpdatePing(SessionInfo session, long ping)
- {
- if (!ContainsSession(session.Id))
- {
- return;
- }
-
- Participants[session.Id].Ping = ping;
- }
-
- /// <summary>
- /// Gets the highest ping in the group.
- /// </summary>
- /// <value name="session">The highest ping in the group.</value>
- public long GetHighestPing()
- {
- long max = long.MinValue;
- foreach (var session in Participants.Values)
- {
- max = Math.Max(max, session.Ping);
- }
-
- return max;
- }
-
- /// <summary>
- /// Sets the session's buffering state.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="isBuffering">The state.</param>
- public void SetBuffering(SessionInfo session, bool isBuffering)
- {
- if (!ContainsSession(session.Id))
- {
- return;
- }
-
- Participants[session.Id].IsBuffering = isBuffering;
- }
-
- /// <summary>
- /// Gets the group buffering state.
- /// </summary>
- /// <value><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</value>
- public bool IsBuffering()
- {
- foreach (var session in Participants.Values)
- {
- if (session.IsBuffering)
- {
- return true;
- }
- }
-
- return false;
- }
-
- /// <summary>
- /// Checks if the group is empty.
- /// </summary>
- /// <value><c>true</c> if the group is empty; <c>false</c> otherwise.</value>
- public bool IsEmpty()
- {
- return Participants.Count == 0;
- }
- }
-}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
index cde6f8e8ce..7e7e759a51 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupMember.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using MediaBrowser.Controller.Session;
namespace MediaBrowser.Controller.SyncPlay
@@ -8,21 +10,36 @@ namespace MediaBrowser.Controller.SyncPlay
public class GroupMember
{
/// <summary>
- /// Gets or sets a value indicating whether this member is buffering.
+ /// Initializes a new instance of the <see cref="GroupMember"/> class.
/// </summary>
- /// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value>
- public bool IsBuffering { get; set; }
+ /// <param name="session">The session.</param>
+ public GroupMember(SessionInfo session)
+ {
+ Session = session;
+ }
/// <summary>
- /// Gets or sets the session.
+ /// Gets the session.
/// </summary>
/// <value>The session.</value>
- public SessionInfo Session { get; set; }
+ public SessionInfo Session { get; }
/// <summary>
- /// Gets or sets the ping.
+ /// Gets or sets the ping, in milliseconds.
/// </summary>
/// <value>The ping.</value>
public long Ping { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this member is buffering.
+ /// </summary>
+ /// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value>
+ public bool IsBuffering { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this member is following group playback.
+ /// </summary>
+ /// <value><c>true</c> to ignore member on group wait; <c>false</c> if they're following group playback.</value>
+ public bool IgnoreGroupWait { get; set; }
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
new file mode 100644
index 0000000000..91a13fb28e
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
@@ -0,0 +1,224 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.SyncPlay.GroupStates
+{
+ /// <summary>
+ /// Class AbstractGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public abstract class AbstractGroupState : IGroupState
+ {
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<AbstractGroupState> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AbstractGroupState"/> class.
+ /// </summary>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ protected AbstractGroupState(ILoggerFactory loggerFactory)
+ {
+ LoggerFactory = loggerFactory;
+ _logger = loggerFactory.CreateLogger<AbstractGroupState>();
+ }
+
+ /// <inheritdoc />
+ public abstract GroupStateType Type { get; }
+
+ /// <summary>
+ /// Gets the logger factory.
+ /// </summary>
+ protected ILoggerFactory LoggerFactory { get; }
+
+ /// <inheritdoc />
+ public abstract void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <inheritdoc />
+ public abstract void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(IGroupPlaybackRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(SetPlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(RemoveFromPlaylistGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var playingItemRemoved = context.RemoveFromPlayQueue(request.PlaylistItemIds);
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RemoveItems);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ if (playingItemRemoved && !context.PlayQueue.IsItemPlaying())
+ {
+ _logger.LogDebug("Play queue in group {GroupId} is now empty.", context.GroupId.ToString());
+
+ IGroupState idleState = new IdleGroupState(LoggerFactory);
+ context.SetState(idleState);
+ var stopRequest = new StopGroupRequest();
+ idleState.HandleRequest(stopRequest, context, Type, session, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(MovePlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var result = context.MoveItemInPlayQueue(request.PlaylistItemId, request.NewIndex);
+
+ if (!result)
+ {
+ _logger.LogError("Unable to move item in group {GroupId}.", context.GroupId.ToString());
+ return;
+ }
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.MoveItem);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(QueueGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var result = context.AddToPlayQueue(request.ItemIds, request.Mode);
+
+ if (!result)
+ {
+ _logger.LogError("Unable to add items to play queue in group {GroupId}.", context.GroupId.ToString());
+ return;
+ }
+
+ var reason = request.Mode switch
+ {
+ GroupQueueMode.QueueNext => PlayQueueUpdateReason.QueueNext,
+ _ => PlayQueueUpdateReason.Queue
+ };
+ var playQueueUpdate = context.GetPlayQueueUpdate(reason);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(SetRepeatModeGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ context.SetRepeatMode(request.Mode);
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RepeatMode);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(SetShuffleModeGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ context.SetShuffleMode(request.Mode);
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.ShuffleMode);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(PingGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Collected pings are used to account for network latency when unpausing playback.
+ context.UpdatePing(session, request.Ping);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(IgnoreWaitGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ context.SetIgnoreGroupWait(session, request.IgnoreWait);
+ }
+
+ /// <summary>
+ /// Sends a group state update to all group.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="reason">The reason of the state change.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ protected void SendGroupStateUpdate(IGroupStateContext context, IGroupPlaybackRequest reason, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Notify relevant state change event.
+ var stateUpdate = new GroupStateUpdate(Type, reason.Action);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.StateUpdate, stateUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ private void UnhandledRequest(IGroupPlaybackRequest request)
+ {
+ _logger.LogWarning("Unhandled request of type {RequestType} in {StateType} state.", request.Action, Type);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs
new file mode 100644
index 0000000000..6b5a7cdf9a
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs
@@ -0,0 +1,128 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.SyncPlay.GroupStates
+{
+ /// <summary>
+ /// Class IdleGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class IdleGroupState : AbstractGroupState
+ {
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<IdleGroupState> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IdleGroupState"/> class.
+ /// </summary>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public IdleGroupState(ILoggerFactory loggerFactory)
+ : base(loggerFactory)
+ {
+ _logger = LoggerFactory.CreateLogger<IdleGroupState>();
+ }
+
+ /// <inheritdoc />
+ public override GroupStateType Type { get; } = GroupStateType.Idle;
+
+ /// <inheritdoc />
+ public override void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Do nothing.
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ private void SendStopCommand(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var command = context.NewSyncPlayCommand(SendCommandType.Stop);
+ if (!prevState.Equals(Type))
+ {
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+ }
+ else
+ {
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs
new file mode 100644
index 0000000000..b9786ddb08
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs
@@ -0,0 +1,167 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.SyncPlay.GroupStates
+{
+ /// <summary>
+ /// Class PausedGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class PausedGroupState : AbstractGroupState
+ {
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<PausedGroupState> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PausedGroupState"/> class.
+ /// </summary>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public PausedGroupState(ILoggerFactory loggerFactory)
+ : base(loggerFactory)
+ {
+ _logger = LoggerFactory.CreateLogger<PausedGroupState>();
+ }
+
+ /// <inheritdoc />
+ public override GroupStateType Type { get; } = GroupStateType.Paused;
+
+ /// <inheritdoc />
+ public override void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Wait for session to be ready.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.SessionJoined(context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Do nothing.
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var playingState = new PlayingGroupState(LoggerFactory);
+ context.SetState(playingState);
+ playingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (!prevState.Equals(Type))
+ {
+ // Pause group and compute the media playback position.
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - context.LastActivity;
+ context.LastActivity = currentTime;
+ // Elapsed time is negative if event happens
+ // during the delay added to account for latency.
+ // In this phase clients haven't started the playback yet.
+ // In other words, LastActivity is in the future,
+ // when playback unpause is supposed to happen.
+ // Seek only if playback actually started.
+ context.PositionTicks += Math.Max(elapsedTime.Ticks, 0);
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+ else
+ {
+ // Client got lost, sending current state.
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var idleState = new IdleGroupState(LoggerFactory);
+ context.SetState(idleState);
+ idleState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (prevState.Equals(Type))
+ {
+ // Client got lost, sending current state.
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ else if (prevState.Equals(GroupStateType.Waiting))
+ {
+ // Sending current state to all clients.
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs
new file mode 100644
index 0000000000..cb1cadf0bc
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs
@@ -0,0 +1,170 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.SyncPlay.GroupStates
+{
+ /// <summary>
+ /// Class PlayingGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class PlayingGroupState : AbstractGroupState
+ {
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<PlayingGroupState> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlayingGroupState"/> class.
+ /// </summary>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public PlayingGroupState(ILoggerFactory loggerFactory)
+ : base(loggerFactory)
+ {
+ _logger = LoggerFactory.CreateLogger<PlayingGroupState>();
+ }
+
+ /// <inheritdoc />
+ public override GroupStateType Type { get; } = GroupStateType.Playing;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether requests for buffering should be ignored.
+ /// </summary>
+ public bool IgnoreBuffering { get; set; }
+
+ /// <inheritdoc />
+ public override void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Wait for session to be ready.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.SessionJoined(context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Do nothing.
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (!prevState.Equals(Type))
+ {
+ // Pick a suitable time that accounts for latency.
+ var delayMillis = Math.Max(context.GetHighestPing() * 2, context.DefaultPing);
+
+ // Unpause group and set starting point in future.
+ // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position).
+ // The added delay does not guarantee, of course, that the command will be received in time.
+ // Playback synchronization will mainly happen client side.
+ context.LastActivity = DateTime.UtcNow.AddMilliseconds(delayMillis);
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+ else
+ {
+ // Client got lost, sending current state.
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var pausedState = new PausedGroupState(LoggerFactory);
+ context.SetState(pausedState);
+ pausedState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var idleState = new IdleGroupState(LoggerFactory);
+ context.SetState(idleState);
+ idleState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (IgnoreBuffering)
+ {
+ return;
+ }
+
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (prevState.Equals(Type))
+ {
+ // Group was not waiting, make sure client has latest state.
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ else if (prevState.Equals(GroupStateType.Waiting))
+ {
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state.
+ var waitingState = new WaitingGroupState(LoggerFactory);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
new file mode 100644
index 0000000000..a0c38b3097
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
@@ -0,0 +1,682 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.SyncPlay.GroupStates
+{
+ /// <summary>
+ /// Class WaitingGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class WaitingGroupState : AbstractGroupState
+ {
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger<WaitingGroupState> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WaitingGroupState"/> class.
+ /// </summary>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public WaitingGroupState(ILoggerFactory loggerFactory)
+ : base(loggerFactory)
+ {
+ _logger = LoggerFactory.CreateLogger<WaitingGroupState>();
+ }
+
+ /// <inheritdoc />
+ public override GroupStateType Type { get; } = GroupStateType.Waiting;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether playback should resume when group is ready.
+ /// </summary>
+ public bool ResumePlaying { get; set; } = false;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the initial state has been set.
+ /// </summary>
+ private bool InitialStateSet { get; set; } = false;
+
+ /// <summary>
+ /// Gets or sets the group state before the first ever event.
+ /// </summary>
+ private GroupStateType InitialState { get; set; }
+
+ /// <inheritdoc />
+ public override void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ if (prevState.Equals(GroupStateType.Playing))
+ {
+ ResumePlaying = true;
+ // Pause group and compute the media playback position.
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - context.LastActivity;
+ context.LastActivity = currentTime;
+ // Elapsed time is negative if event happens
+ // during the delay added to account for latency.
+ // In this phase clients haven't started the playback yet.
+ // In other words, LastActivity is in the future,
+ // when playback unpause is supposed to happen.
+ // Seek only if playback actually started.
+ context.PositionTicks += Math.Max(elapsedTime.Ticks, 0);
+ }
+
+ // Prepare new session.
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken);
+
+ context.SetBuffering(session, true);
+
+ // Send pause command to all non-buffering sessions.
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ context.SetBuffering(session, false);
+
+ if (!context.IsBuffering())
+ {
+ if (ResumePlaying)
+ {
+ _logger.LogDebug("Session {SessionId} left group {GroupId}, notifying others to resume.", session.Id, context.GroupId.ToString());
+
+ // Client, that was buffering, left the group.
+ var playingState = new PlayingGroupState(LoggerFactory);
+ context.SetState(playingState);
+ var unpauseRequest = new UnpauseGroupRequest();
+ playingState.HandleRequest(unpauseRequest, context, Type, session, cancellationToken);
+ }
+ else
+ {
+ _logger.LogDebug("Session {SessionId} left group {GroupId}, returning to previous state.", session.Id, context.GroupId.ToString());
+
+ // Group is ready, returning to previous state.
+ var pausedState = new PausedGroupState(LoggerFactory);
+ context.SetState(pausedState);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ ResumePlaying = true;
+
+ var setQueueStatus = context.SetPlayQueue(request.PlayingQueue, request.PlayingItemPosition, request.StartPositionTicks);
+ if (!setQueueStatus)
+ {
+ _logger.LogError("Unable to set playing queue in group {GroupId}.", context.GroupId.ToString());
+
+ // Ignore request and return to previous state.
+ IGroupState newState = prevState switch {
+ GroupStateType.Playing => new PlayingGroupState(LoggerFactory),
+ GroupStateType.Paused => new PausedGroupState(LoggerFactory),
+ _ => new IdleGroupState(LoggerFactory)
+ };
+
+ context.SetState(newState);
+ return;
+ }
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events.
+ context.SetAllBuffering(true);
+
+ _logger.LogDebug("Session {SessionId} set a new play queue in group {GroupId}.", session.Id, context.GroupId.ToString());
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(SetPlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ ResumePlaying = true;
+
+ var result = context.SetPlayingItem(request.PlaylistItemId);
+ if (result)
+ {
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events.
+ context.SetAllBuffering(true);
+ }
+ else
+ {
+ // Return to old state.
+ IGroupState newState = prevState switch
+ {
+ GroupStateType.Playing => new PlayingGroupState(LoggerFactory),
+ GroupStateType.Paused => new PausedGroupState(LoggerFactory),
+ _ => new IdleGroupState(LoggerFactory)
+ };
+
+ context.SetState(newState);
+
+ _logger.LogDebug("Unable to change current playing item in group {GroupId}.", context.GroupId.ToString());
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ if (prevState.Equals(GroupStateType.Idle))
+ {
+ ResumePlaying = true;
+ context.RestartCurrentItem();
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events.
+ context.SetAllBuffering(true);
+
+ _logger.LogDebug("Group {GroupId} is waiting for all ready events.", context.GroupId.ToString());
+ }
+ else
+ {
+ if (ResumePlaying)
+ {
+ _logger.LogDebug("Forcing the playback to start in group {GroupId}. Group-wait is disabled until next state change.", context.GroupId.ToString());
+
+ // An Unpause request is forcing the playback to start, ignoring sessions that are not ready.
+ context.SetAllBuffering(false);
+
+ // Change state.
+ var playingState = new PlayingGroupState(LoggerFactory)
+ {
+ IgnoreBuffering = true
+ };
+ context.SetState(playingState);
+ playingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+ else
+ {
+ // Group would have gone to paused state, now will go to playing state when ready.
+ ResumePlaying = true;
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ // Wait for sessions to be ready, then switch to paused state.
+ ResumePlaying = false;
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ // Change state.
+ var idleState = new IdleGroupState(LoggerFactory);
+ context.SetState(idleState);
+ idleState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ if (prevState.Equals(GroupStateType.Playing))
+ {
+ ResumePlaying = true;
+ }
+ else if (prevState.Equals(GroupStateType.Paused))
+ {
+ ResumePlaying = false;
+ }
+
+ // Sanitize PositionTicks.
+ var ticks = context.SanitizePositionTicks(request.PositionTicks);
+
+ // Seek.
+ context.PositionTicks = ticks;
+ context.LastActivity = DateTime.UtcNow;
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Seek);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events.
+ context.SetAllBuffering(true);
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ // Make sure the client is playing the correct item.
+ if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId()))
+ {
+ _logger.LogDebug("Session {SessionId} reported wrong playlist item in group {GroupId}.", session.Id, context.GroupId.ToString());
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
+ var updateSession = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
+ context.SetBuffering(session, true);
+
+ return;
+ }
+
+ if (prevState.Equals(GroupStateType.Playing))
+ {
+ // Resume playback when all ready.
+ ResumePlaying = true;
+
+ context.SetBuffering(session, true);
+
+ // Pause group and compute the media playback position.
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - context.LastActivity;
+ context.LastActivity = currentTime;
+ // Elapsed time is negative if event happens
+ // during the delay added to account for latency.
+ // In this phase clients haven't started the playback yet.
+ // In other words, LastActivity is in the future,
+ // when playback unpause is supposed to happen.
+ // Seek only if playback actually started.
+ context.PositionTicks += Math.Max(elapsedTime.Ticks, 0);
+
+ // Send pause command to all non-buffering sessions.
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken);
+ }
+ else if (prevState.Equals(GroupStateType.Paused))
+ {
+ // Don't resume playback when all ready.
+ ResumePlaying = false;
+
+ context.SetBuffering(session, true);
+
+ // Send pause command to buffering session.
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ else if (prevState.Equals(GroupStateType.Waiting))
+ {
+ // Another session is now buffering.
+ context.SetBuffering(session, true);
+
+ if (!ResumePlaying)
+ {
+ // Force update for this session that should be paused.
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ // Make sure the client is playing the correct item.
+ if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId()))
+ {
+ _logger.LogDebug("Session {SessionId} reported wrong playlist item in group {GroupId}.", session.Id, context.GroupId.ToString());
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken);
+ context.SetBuffering(session, true);
+
+ return;
+ }
+
+ // Compute elapsed time between the client reported time and now.
+ // Elapsed time is used to estimate the client position when playback is unpaused.
+ // Ideally, the request is received and handled without major delays.
+ // However, to avoid waiting indefinitely when a client is not reporting a correct time,
+ // the elapsed time is ignored after a certain threshold.
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime.Subtract(request.When);
+ var timeSyncThresholdTicks = TimeSpan.FromMilliseconds(context.TimeSyncOffset).Ticks;
+ if (Math.Abs(elapsedTime.Ticks) > timeSyncThresholdTicks)
+ {
+ _logger.LogWarning("Session {SessionId} is not time syncing properly. Ignoring elapsed time.", session.Id);
+
+ elapsedTime = TimeSpan.Zero;
+ }
+
+ // Ignore elapsed time if client is paused.
+ if (!request.IsPlaying)
+ {
+ elapsedTime = TimeSpan.Zero;
+ }
+
+ var requestTicks = context.SanitizePositionTicks(request.PositionTicks);
+ var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime;
+ var delayTicks = context.PositionTicks - clientPosition.Ticks;
+ var maxPlaybackOffsetTicks = TimeSpan.FromMilliseconds(context.MaxPlaybackOffset).Ticks;
+
+ _logger.LogDebug("Session {SessionId} is at {PositionTicks} (delay of {Delay} seconds) in group {GroupId}.", session.Id, clientPosition, TimeSpan.FromTicks(delayTicks).TotalSeconds, context.GroupId.ToString());
+
+ if (ResumePlaying)
+ {
+ // Handle case where session reported as ready but in reality
+ // it has no clue of the real position nor the playback state.
+ if (!request.IsPlaying && Math.Abs(delayTicks) > maxPlaybackOffsetTicks)
+ {
+ // Session not ready at all.
+ context.SetBuffering(session, true);
+
+ // Correcting session's position.
+ var command = context.NewSyncPlayCommand(SendCommandType.Seek);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+
+ _logger.LogWarning("Session {SessionId} got lost in time, correcting.", session.Id);
+ return;
+ }
+
+ // Session is ready.
+ context.SetBuffering(session, false);
+
+ if (context.IsBuffering())
+ {
+ // Others are still buffering, tell this client to pause when ready.
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ command.When = currentTime.AddTicks(delayTicks);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+
+ _logger.LogInformation("Session {SessionId} will pause when ready in {Delay} seconds. Group {GroupId} is waiting for all ready events.", session.Id, TimeSpan.FromTicks(delayTicks).TotalSeconds, context.GroupId.ToString());
+ }
+ else
+ {
+ // If all ready, then start playback.
+ // Let other clients resume as soon as the buffering client catches up.
+ if (delayTicks > context.GetHighestPing() * 2 * TimeSpan.TicksPerMillisecond)
+ {
+ // Client that was buffering is recovering, notifying others to resume.
+ context.LastActivity = currentTime.AddTicks(delayTicks);
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
+ var filter = SyncPlayBroadcastType.AllExceptCurrentSession;
+ if (!request.IsPlaying)
+ {
+ filter = SyncPlayBroadcastType.AllGroup;
+ }
+
+ context.SendCommand(session, filter, command, cancellationToken);
+
+ _logger.LogInformation("Session {SessionId} is recovering, group {GroupId} will resume in {Delay} seconds.", session.Id, context.GroupId.ToString(), TimeSpan.FromTicks(delayTicks).TotalSeconds);
+ }
+ else
+ {
+ // Client, that was buffering, resumed playback but did not update others in time.
+ delayTicks = context.GetHighestPing() * 2 * TimeSpan.TicksPerMillisecond;
+ delayTicks = Math.Max(delayTicks, context.DefaultPing);
+
+ context.LastActivity = currentTime.AddTicks(delayTicks);
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ _logger.LogWarning("Session {SessionId} resumed playback, group {GroupId} has {Delay} seconds to recover.", session.Id, context.GroupId.ToString(), TimeSpan.FromTicks(delayTicks).TotalSeconds);
+ }
+
+ // Change state.
+ var playingState = new PlayingGroupState(LoggerFactory);
+ context.SetState(playingState);
+ playingState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+ }
+ else
+ {
+ // Check that session is really ready, tolerate player imperfections under a certain threshold.
+ if (Math.Abs(context.PositionTicks - requestTicks) > maxPlaybackOffsetTicks)
+ {
+ // Session still not ready.
+ context.SetBuffering(session, true);
+ // Session is seeking to wrong position, correcting.
+ var command = context.NewSyncPlayCommand(SendCommandType.Seek);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+
+ // Notify relevant state change event.
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+
+ _logger.LogWarning("Session {SessionId} is seeking to wrong position, correcting.", session.Id);
+ return;
+ }
+ else
+ {
+ // Session is ready.
+ context.SetBuffering(session, false);
+ }
+
+ if (!context.IsBuffering())
+ {
+ _logger.LogDebug("Session {SessionId} is ready, group {GroupId} is ready.", session.Id, context.GroupId.ToString());
+
+ // Group is ready, returning to previous state.
+ var pausedState = new PausedGroupState(LoggerFactory);
+ context.SetState(pausedState);
+
+ if (InitialState.Equals(GroupStateType.Playing))
+ {
+ // Group went from playing to waiting state and a pause request occured while waiting.
+ var pauseRequest = new PauseGroupRequest();
+ pausedState.HandleRequest(pauseRequest, context, Type, session, cancellationToken);
+ }
+ else if (InitialState.Equals(GroupStateType.Paused))
+ {
+ pausedState.HandleRequest(request, context, Type, session, cancellationToken);
+ }
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ ResumePlaying = true;
+
+ // Make sure the client knows the playing item, to avoid duplicate requests.
+ if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId()))
+ {
+ _logger.LogDebug("Session {SessionId} provided the wrong playlist item for group {GroupId}.", session.Id, context.GroupId.ToString());
+ return;
+ }
+
+ var newItem = context.NextItemInQueue();
+ if (newItem)
+ {
+ // Send playing-queue update.
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NextItem);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events.
+ context.SetAllBuffering(true);
+ }
+ else
+ {
+ // Return to old state.
+ IGroupState newState = prevState switch
+ {
+ GroupStateType.Playing => new PlayingGroupState(LoggerFactory),
+ GroupStateType.Paused => new PausedGroupState(LoggerFactory),
+ _ => new IdleGroupState(LoggerFactory)
+ };
+
+ context.SetState(newState);
+
+ _logger.LogDebug("No next item available in group {GroupId}.", context.GroupId.ToString());
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event.
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ ResumePlaying = true;
+
+ // Make sure the client knows the playing item, to avoid duplicate requests.
+ if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId()))
+ {
+ _logger.LogDebug("Session {SessionId} provided the wrong playlist item for group {GroupId}.", session.Id, context.GroupId.ToString());
+ return;
+ }
+
+ var newItem = context.PreviousItemInQueue();
+ if (newItem)
+ {
+ // Send playing-queue update.
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.PreviousItem);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events.
+ context.SetAllBuffering(true);
+ }
+ else
+ {
+ // Return to old state.
+ IGroupState newState = prevState switch
+ {
+ GroupStateType.Playing => new PlayingGroupState(LoggerFactory),
+ GroupStateType.Paused => new PausedGroupState(LoggerFactory),
+ _ => new IdleGroupState(LoggerFactory)
+ };
+
+ context.SetState(newState);
+
+ _logger.LogDebug("No previous item available in group {GroupId}.", context.GroupId.ToString());
+ }
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(IgnoreWaitGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ context.SetIgnoreGroupWait(session, request.IgnoreWait);
+
+ if (!context.IsBuffering())
+ {
+ _logger.LogDebug("Ignoring session {SessionId}, group {GroupId} is ready.", session.Id, context.GroupId.ToString());
+
+ if (ResumePlaying)
+ {
+ // Client, that was buffering, stopped following playback.
+ var playingState = new PlayingGroupState(LoggerFactory);
+ context.SetState(playingState);
+ var unpauseRequest = new UnpauseGroupRequest();
+ playingState.HandleRequest(unpauseRequest, context, Type, session, cancellationToken);
+ }
+ else
+ {
+ // Group is ready, returning to previous state.
+ var pausedState = new PausedGroupState(LoggerFactory);
+ context.SetState(pausedState);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs b/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs
new file mode 100644
index 0000000000..9045063eed
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs
@@ -0,0 +1,29 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Interface IGroupPlaybackRequest.
+ /// </summary>
+ public interface IGroupPlaybackRequest : ISyncPlayRequest
+ {
+ /// <summary>
+ /// Gets the playback request type.
+ /// </summary>
+ /// <returns>The playback request type.</returns>
+ PlaybackRequestType Action { get; }
+
+ /// <summary>
+ /// Applies the request to a group.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="state">The current state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupState.cs b/MediaBrowser.Controller/SyncPlay/IGroupState.cs
new file mode 100644
index 0000000000..0666a62a85
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/IGroupState.cs
@@ -0,0 +1,219 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Interface IGroupState.
+ /// </summary>
+ public interface IGroupState
+ {
+ /// <summary>
+ /// Gets the group state type.
+ /// </summary>
+ /// <value>The group state type.</value>
+ GroupStateType Type { get; }
+
+ /// <summary>
+ /// Handles a session that joined the group.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SessionJoined(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a session that is leaving the group.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SessionLeaving(IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Generic handler. Context's state can change.
+ /// </summary>
+ /// <param name="request">The generic request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(IGroupPlaybackRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a play request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The play request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(PlayGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a set-playlist-item request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The set-playlist-item request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(SetPlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a remove-items request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The remove-items request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(RemoveFromPlaylistGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a move-playlist-item request from a session. Context's state should not change.
+ /// </summary>
+ /// <param name="request">The move-playlist-item request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(MovePlaylistItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a queue request from a session. Context's state should not change.
+ /// </summary>
+ /// <param name="request">The queue request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(QueueGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles an unpause request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The unpause request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(UnpauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a pause request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The pause request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(PauseGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a stop request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The stop request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(StopGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a seek request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The seek request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(SeekGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a buffer request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The buffer request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(BufferGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a ready request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The ready request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(ReadyGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a next-item request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The next-item request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(NextItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a previous-item request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The previous-item request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(PreviousItemGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a set-repeat-mode request from a session. Context's state should not change.
+ /// </summary>
+ /// <param name="request">The repeat-mode request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(SetRepeatModeGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a set-shuffle-mode request from a session. Context's state should not change.
+ /// </summary>
+ /// <param name="request">The shuffle-mode request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(SetShuffleModeGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Updates the ping of a session. Context's state should not change.
+ /// </summary>
+ /// <param name="request">The ping request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(PingGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a ignore-wait request from a session. Context's state can change.
+ /// </summary>
+ /// <param name="request">The ignore-wait request.</param>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="prevState">The previous state.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void HandleRequest(IgnoreWaitGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
new file mode 100644
index 0000000000..de26c7d9ef
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
@@ -0,0 +1,224 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay.Queue;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Interface IGroupStateContext.
+ /// </summary>
+ public interface IGroupStateContext
+ {
+ /// <summary>
+ /// Gets the default ping value used for sessions, in milliseconds.
+ /// </summary>
+ /// <value>The default ping value used for sessions, in milliseconds.</value>
+ long DefaultPing { get; }
+
+ /// <summary>
+ /// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds.
+ /// </summary>
+ /// <value>The maximum offset error accepted, in milliseconds.</value>
+ long TimeSyncOffset { get; }
+
+ /// <summary>
+ /// Gets the maximum offset error accepted for position reported by clients, in milliseconds.
+ /// </summary>
+ /// <value>The maximum offset error accepted, in milliseconds.</value>
+ long MaxPlaybackOffset { get; }
+
+ /// <summary>
+ /// Gets the group identifier.
+ /// </summary>
+ /// <value>The group identifier.</value>
+ Guid GroupId { get; }
+
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ long PositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last activity.
+ /// </summary>
+ /// <value>The last activity.</value>
+ DateTime LastActivity { get; set; }
+
+ /// <summary>
+ /// Gets the play queue.
+ /// </summary>
+ /// <value>The play queue.</value>
+ PlayQueueManager PlayQueue { get; }
+
+ /// <summary>
+ /// Sets a new state.
+ /// </summary>
+ /// <param name="state">The new state.</param>
+ void SetState(IGroupState state);
+
+ /// <summary>
+ /// Sends a GroupUpdate message to the interested sessions.
+ /// </summary>
+ /// <typeparam name="T">The type of the data of the message.</typeparam>
+ /// <param name="from">The current session.</param>
+ /// <param name="type">The filtering type.</param>
+ /// <param name="message">The message to send.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The task.</returns>
+ Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends a playback command to the interested sessions.
+ /// </summary>
+ /// <param name="from">The current session.</param>
+ /// <param name="type">The filtering type.</param>
+ /// <param name="message">The message to send.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The task.</returns>
+ Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Builds a new playback command with some default values.
+ /// </summary>
+ /// <param name="type">The command type.</param>
+ /// <returns>The command.</returns>
+ SendCommand NewSyncPlayCommand(SendCommandType type);
+
+ /// <summary>
+ /// Builds a new group update message.
+ /// </summary>
+ /// <typeparam name="T">The type of the data of the message.</typeparam>
+ /// <param name="type">The update type.</param>
+ /// <param name="data">The data to send.</param>
+ /// <returns>The group update.</returns>
+ GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data);
+
+ /// <summary>
+ /// Sanitizes the PositionTicks, considers the current playing item when available.
+ /// </summary>
+ /// <param name="positionTicks">The PositionTicks.</param>
+ /// <returns>The sanitized position ticks.</returns>
+ long SanitizePositionTicks(long? positionTicks);
+
+ /// <summary>
+ /// Updates the ping of a session, in milliseconds.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="ping">The ping, in milliseconds.</param>
+ void UpdatePing(SessionInfo session, long ping);
+
+ /// <summary>
+ /// Gets the highest ping in the group, in milliseconds.
+ /// </summary>
+ /// <returns>The highest ping in the group.</returns>
+ long GetHighestPing();
+
+ /// <summary>
+ /// Sets the session's buffering state.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="isBuffering">The state.</param>
+ void SetBuffering(SessionInfo session, bool isBuffering);
+
+ /// <summary>
+ /// Sets the buffering state of all the sessions.
+ /// </summary>
+ /// <param name="isBuffering">The state.</param>
+ void SetAllBuffering(bool isBuffering);
+
+ /// <summary>
+ /// Gets the group buffering state.
+ /// </summary>
+ /// <returns><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</returns>
+ bool IsBuffering();
+
+ /// <summary>
+ /// Sets the session's group wait state.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="ignoreGroupWait">The state.</param>
+ void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait);
+
+ /// <summary>
+ /// Sets a new play queue.
+ /// </summary>
+ /// <param name="playQueue">The new play queue.</param>
+ /// <param name="playingItemPosition">The playing item position in the play queue.</param>
+ /// <param name="startPositionTicks">The start position ticks.</param>
+ /// <returns><c>true</c> if the play queue has been changed; <c>false</c> if something went wrong.</returns>
+ bool SetPlayQueue(IReadOnlyList<Guid> playQueue, int playingItemPosition, long startPositionTicks);
+
+ /// <summary>
+ /// Sets the playing item.
+ /// </summary>
+ /// <param name="playlistItemId">The new playing item identifier.</param>
+ /// <returns><c>true</c> if the play queue has been changed; <c>false</c> if something went wrong.</returns>
+ bool SetPlayingItem(Guid playlistItemId);
+
+ /// <summary>
+ /// Removes items from the play queue.
+ /// </summary>
+ /// <param name="playlistItemIds">The items to remove.</param>
+ /// <returns><c>true</c> if playing item got removed; <c>false</c> otherwise.</returns>
+ bool RemoveFromPlayQueue(IReadOnlyList<Guid> playlistItemIds);
+
+ /// <summary>
+ /// Moves an item in the play queue.
+ /// </summary>
+ /// <param name="playlistItemId">The playlist identifier of the item to move.</param>
+ /// <param name="newIndex">The new position.</param>
+ /// <returns><c>true</c> if item has been moved; <c>false</c> if something went wrong.</returns>
+ bool MoveItemInPlayQueue(Guid playlistItemId, int newIndex);
+
+ /// <summary>
+ /// Updates the play queue.
+ /// </summary>
+ /// <param name="newItems">The new items to add to the play queue.</param>
+ /// <param name="mode">The mode with which the items will be added.</param>
+ /// <returns><c>true</c> if the play queue has been changed; <c>false</c> if something went wrong.</returns>
+ bool AddToPlayQueue(IReadOnlyList<Guid> newItems, GroupQueueMode mode);
+
+ /// <summary>
+ /// Restarts current item in play queue.
+ /// </summary>
+ void RestartCurrentItem();
+
+ /// <summary>
+ /// Picks next item in play queue.
+ /// </summary>
+ /// <returns><c>true</c> if the item changed; <c>false</c> otherwise.</returns>
+ bool NextItemInQueue();
+
+ /// <summary>
+ /// Picks previous item in play queue.
+ /// </summary>
+ /// <returns><c>true</c> if the item changed; <c>false</c> otherwise.</returns>
+ bool PreviousItemInQueue();
+
+ /// <summary>
+ /// Sets the repeat mode.
+ /// </summary>
+ /// <param name="mode">The new mode.</param>
+ void SetRepeatMode(GroupRepeatMode mode);
+
+ /// <summary>
+ /// Sets the shuffle mode.
+ /// </summary>
+ /// <param name="mode">The new mode.</param>
+ void SetShuffleMode(GroupShuffleMode mode);
+
+ /// <summary>
+ /// Creates a play queue update.
+ /// </summary>
+ /// <param name="reason">The reason for the update.</param>
+ /// <returns>The play queue update.</returns>
+ PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason);
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
deleted file mode 100644
index 60d17fe367..0000000000
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using System;
-using System.Threading;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.SyncPlay;
-
-namespace MediaBrowser.Controller.SyncPlay
-{
- /// <summary>
- /// Interface ISyncPlayController.
- /// </summary>
- public interface ISyncPlayController
- {
- /// <summary>
- /// Gets the group id.
- /// </summary>
- /// <value>The group id.</value>
- Guid GetGroupId();
-
- /// <summary>
- /// Gets the playing item id.
- /// </summary>
- /// <value>The playing item id.</value>
- Guid GetPlayingItemId();
-
- /// <summary>
- /// Checks if the group is empty.
- /// </summary>
- /// <value>If the group is empty.</value>
- bool IsGroupEmpty();
-
- /// <summary>
- /// Initializes the group with the session's info.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void CreateGroup(SessionInfo session, CancellationToken cancellationToken);
-
- /// <summary>
- /// Adds the session to the group.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken);
-
- /// <summary>
- /// Removes the session from the group.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SessionLeave(SessionInfo session, CancellationToken cancellationToken);
-
- /// <summary>
- /// Handles the requested action by the session.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="request">The requested action.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken);
-
- /// <summary>
- /// Gets the info about the group for the clients.
- /// </summary>
- /// <value>The group info for the clients.</value>
- GroupInfoView GetInfo();
- }
-}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
index 006fb687b8..a6999a12c9 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
@@ -1,7 +1,10 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay.Requests;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.SyncPlay
@@ -15,32 +18,33 @@ namespace MediaBrowser.Controller.SyncPlay
/// Creates a new group.
/// </summary>
/// <param name="session">The session that's creating the group.</param>
+ /// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void NewGroup(SessionInfo session, CancellationToken cancellationToken);
+ void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken);
/// <summary>
/// Adds the session to a group.
/// </summary>
/// <param name="session">The session.</param>
- /// <param name="groupId">The group id.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void JoinGroup(SessionInfo session, Guid groupId, JoinGroupRequest request, CancellationToken cancellationToken);
+ void JoinGroup(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken);
/// <summary>
/// Removes the session from a group.
/// </summary>
/// <param name="session">The session.</param>
+ /// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void LeaveGroup(SessionInfo session, CancellationToken cancellationToken);
+ void LeaveGroup(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken);
/// <summary>
/// Gets list of available groups for a session.
/// </summary>
/// <param name="session">The session.</param>
- /// <param name="filterItemId">The item id to filter by.</param>
- /// <value>The list of available groups.</value>
- List<GroupInfoView> ListGroups(SessionInfo session, Guid filterItemId);
+ /// <param name="request">The request.</param>
+ /// <returns>The list of available groups.</returns>
+ List<GroupInfoDto> ListGroups(SessionInfo session, ListGroupsRequest request);
/// <summary>
/// Handle a request by a session in a group.
@@ -48,22 +52,13 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken);
+ void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken);
/// <summary>
- /// Maps a session to a group.
+ /// Checks whether a user has an active session using SyncPlay.
/// </summary>
- /// <param name="session">The session.</param>
- /// <param name="group">The group.</param>
- /// <exception cref="InvalidOperationException"></exception>
- void AddSessionToGroup(SessionInfo session, ISyncPlayController group);
-
- /// <summary>
- /// Unmaps a session from a group.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="group">The group.</param>
- /// <exception cref="InvalidOperationException"></exception>
- void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group);
+ /// <param name="userId">The user identifier to check.</param>
+ /// <returns><c>true</c> if the user is using SyncPlay; <c>false</c> otherwise.</returns>
+ bool IsUserActive(Guid userId);
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayRequest.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayRequest.cs
new file mode 100644
index 0000000000..bf19817732
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayRequest.cs
@@ -0,0 +1,16 @@
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Interface ISyncPlayRequest.
+ /// </summary>
+ public interface ISyncPlayRequest
+ {
+ /// <summary>
+ /// Gets the request type.
+ /// </summary>
+ /// <returns>The request type.</returns>
+ RequestType Type { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs
new file mode 100644
index 0000000000..ef496c1038
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs
@@ -0,0 +1,31 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class AbstractPlaybackRequest.
+ /// </summary>
+ public abstract class AbstractPlaybackRequest : IGroupPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AbstractPlaybackRequest"/> class.
+ /// </summary>
+ protected AbstractPlaybackRequest()
+ {
+ // Do nothing.
+ }
+
+ /// <inheritdoc />
+ public RequestType Type { get; } = RequestType.Playback;
+
+ /// <inheritdoc />
+ public abstract PlaybackRequestType Action { get; }
+
+ /// <inheritdoc />
+ public abstract void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs
new file mode 100644
index 0000000000..d188114c34
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs
@@ -0,0 +1,63 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class BufferGroupRequest.
+ /// </summary>
+ public class BufferGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BufferGroupRequest"/> class.
+ /// </summary>
+ /// <param name="when">When the request has been made, as reported by the client.</param>
+ /// <param name="positionTicks">The position ticks.</param>
+ /// <param name="isPlaying">Whether the client playback is unpaused.</param>
+ /// <param name="playlistItemId">The playlist item identifier of the playing item.</param>
+ public BufferGroupRequest(DateTime when, long positionTicks, bool isPlaying, Guid playlistItemId)
+ {
+ When = when;
+ PositionTicks = positionTicks;
+ IsPlaying = isPlaying;
+ PlaylistItemId = playlistItemId;
+ }
+
+ /// <summary>
+ /// Gets when the request has been made by the client.
+ /// </summary>
+ /// <value>The date of the request.</value>
+ public DateTime When { get; }
+
+ /// <summary>
+ /// Gets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the client playback is unpaused.
+ /// </summary>
+ /// <value>The client playback status.</value>
+ public bool IsPlaying { get; }
+
+ /// <summary>
+ /// Gets the playlist item identifier of the playing item.
+ /// </summary>
+ /// <value>The playlist item identifier.</value>
+ public Guid PlaylistItemId { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Buffer;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs
new file mode 100644
index 0000000000..464c81dfd2
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs
@@ -0,0 +1,38 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class IgnoreWaitGroupRequest.
+ /// </summary>
+ public class IgnoreWaitGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgnoreWaitGroupRequest"/> class.
+ /// </summary>
+ /// <param name="ignoreWait">Whether the client should be ignored.</param>
+ public IgnoreWaitGroupRequest(bool ignoreWait)
+ {
+ IgnoreWait = ignoreWait;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the client should be ignored.
+ /// </summary>
+ /// <value>The client group-wait status.</value>
+ public bool IgnoreWait { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.IgnoreWait;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs
new file mode 100644
index 0000000000..be314e807b
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs
@@ -0,0 +1,47 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class MovePlaylistItemGroupRequest.
+ /// </summary>
+ public class MovePlaylistItemGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MovePlaylistItemGroupRequest"/> class.
+ /// </summary>
+ /// <param name="playlistItemId">The playlist identifier of the item.</param>
+ /// <param name="newIndex">The new position.</param>
+ public MovePlaylistItemGroupRequest(Guid playlistItemId, int newIndex)
+ {
+ PlaylistItemId = playlistItemId;
+ NewIndex = newIndex;
+ }
+
+ /// <summary>
+ /// Gets the playlist identifier of the item.
+ /// </summary>
+ /// <value>The playlist identifier of the item.</value>
+ public Guid PlaylistItemId { get; }
+
+ /// <summary>
+ /// Gets the new position.
+ /// </summary>
+ /// <value>The new position.</value>
+ public int NewIndex { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.MovePlaylistItem;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs
new file mode 100644
index 0000000000..679076239e
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs
@@ -0,0 +1,39 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class NextItemGroupRequest.
+ /// </summary>
+ public class NextItemGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NextItemGroupRequest"/> class.
+ /// </summary>
+ /// <param name="playlistItemId">The playing item identifier.</param>
+ public NextItemGroupRequest(Guid playlistItemId)
+ {
+ PlaylistItemId = playlistItemId;
+ }
+
+ /// <summary>
+ /// Gets the playing item identifier.
+ /// </summary>
+ /// <value>The playing item identifier.</value>
+ public Guid PlaylistItemId { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.NextItem;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs
new file mode 100644
index 0000000000..7ee18a3666
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs
@@ -0,0 +1,23 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class PauseGroupRequest.
+ /// </summary>
+ public class PauseGroupRequest : AbstractPlaybackRequest
+ {
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Pause;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs
new file mode 100644
index 0000000000..beab655c59
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs
@@ -0,0 +1,38 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class PingGroupRequest.
+ /// </summary>
+ public class PingGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PingGroupRequest"/> class.
+ /// </summary>
+ /// <param name="ping">The ping time.</param>
+ public PingGroupRequest(long ping)
+ {
+ Ping = ping;
+ }
+
+ /// <summary>
+ /// Gets the ping time.
+ /// </summary>
+ /// <value>The ping time.</value>
+ public long Ping { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Ping;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs
new file mode 100644
index 0000000000..05ff262c1e
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs
@@ -0,0 +1,56 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class PlayGroupRequest.
+ /// </summary>
+ public class PlayGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlayGroupRequest"/> class.
+ /// </summary>
+ /// <param name="playingQueue">The playing queue.</param>
+ /// <param name="playingItemPosition">The playing item position.</param>
+ /// <param name="startPositionTicks">The start position ticks.</param>
+ public PlayGroupRequest(IReadOnlyList<Guid> playingQueue, int playingItemPosition, long startPositionTicks)
+ {
+ PlayingQueue = playingQueue ?? Array.Empty<Guid>();
+ PlayingItemPosition = playingItemPosition;
+ StartPositionTicks = startPositionTicks;
+ }
+
+ /// <summary>
+ /// Gets the playing queue.
+ /// </summary>
+ /// <value>The playing queue.</value>
+ public IReadOnlyList<Guid> PlayingQueue { get; }
+
+ /// <summary>
+ /// Gets the position of the playing item in the queue.
+ /// </summary>
+ /// <value>The playing item position.</value>
+ public int PlayingItemPosition { get; }
+
+ /// <summary>
+ /// Gets the start position ticks.
+ /// </summary>
+ /// <value>The start position ticks.</value>
+ public long StartPositionTicks { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Play;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs
new file mode 100644
index 0000000000..3e34b6ce46
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs
@@ -0,0 +1,39 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class PreviousItemGroupRequest.
+ /// </summary>
+ public class PreviousItemGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PreviousItemGroupRequest"/> class.
+ /// </summary>
+ /// <param name="playlistItemId">The playing item identifier.</param>
+ public PreviousItemGroupRequest(Guid playlistItemId)
+ {
+ PlaylistItemId = playlistItemId;
+ }
+
+ /// <summary>
+ /// Gets the playing item identifier.
+ /// </summary>
+ /// <value>The playing item identifier.</value>
+ public Guid PlaylistItemId { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.PreviousItem;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs
new file mode 100644
index 0000000000..0f91476de1
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs
@@ -0,0 +1,48 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class QueueGroupRequest.
+ /// </summary>
+ public class QueueGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QueueGroupRequest"/> class.
+ /// </summary>
+ /// <param name="items">The items to add to the queue.</param>
+ /// <param name="mode">The enqueue mode.</param>
+ public QueueGroupRequest(IReadOnlyList<Guid> items, GroupQueueMode mode)
+ {
+ ItemIds = items ?? Array.Empty<Guid>();
+ Mode = mode;
+ }
+
+ /// <summary>
+ /// Gets the items to enqueue.
+ /// </summary>
+ /// <value>The items to enqueue.</value>
+ public IReadOnlyList<Guid> ItemIds { get; }
+
+ /// <summary>
+ /// Gets the mode in which to add the new items.
+ /// </summary>
+ /// <value>The enqueue mode.</value>
+ public GroupQueueMode Mode { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Queue;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs
new file mode 100644
index 0000000000..b1f0bd3602
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs
@@ -0,0 +1,63 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class ReadyGroupRequest.
+ /// </summary>
+ public class ReadyGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReadyGroupRequest"/> class.
+ /// </summary>
+ /// <param name="when">When the request has been made, as reported by the client.</param>
+ /// <param name="positionTicks">The position ticks.</param>
+ /// <param name="isPlaying">Whether the client playback is unpaused.</param>
+ /// <param name="playlistItemId">The playlist item identifier of the playing item.</param>
+ public ReadyGroupRequest(DateTime when, long positionTicks, bool isPlaying, Guid playlistItemId)
+ {
+ When = when;
+ PositionTicks = positionTicks;
+ IsPlaying = isPlaying;
+ PlaylistItemId = playlistItemId;
+ }
+
+ /// <summary>
+ /// Gets when the request has been made by the client.
+ /// </summary>
+ /// <value>The date of the request.</value>
+ public DateTime When { get; }
+
+ /// <summary>
+ /// Gets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the client playback is unpaused.
+ /// </summary>
+ /// <value>The client playback status.</value>
+ public bool IsPlaying { get; }
+
+ /// <summary>
+ /// Gets the playlist item identifier of the playing item.
+ /// </summary>
+ /// <value>The playlist item identifier.</value>
+ public Guid PlaylistItemId { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Ready;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
new file mode 100644
index 0000000000..6891452934
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
@@ -0,0 +1,40 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class RemoveFromPlaylistGroupRequest.
+ /// </summary>
+ public class RemoveFromPlaylistGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RemoveFromPlaylistGroupRequest"/> class.
+ /// </summary>
+ /// <param name="items">The playlist ids of the items to remove.</param>
+ public RemoveFromPlaylistGroupRequest(IReadOnlyList<Guid> items)
+ {
+ PlaylistItemIds = items ?? Array.Empty<Guid>();
+ }
+
+ /// <summary>
+ /// Gets the playlist identifiers ot the items.
+ /// </summary>
+ /// <value>The playlist identifiers ot the items.</value>
+ public IReadOnlyList<Guid> PlaylistItemIds { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.RemoveFromPlaylist;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs
new file mode 100644
index 0000000000..1961133749
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs
@@ -0,0 +1,38 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class SeekGroupRequest.
+ /// </summary>
+ public class SeekGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeekGroupRequest"/> class.
+ /// </summary>
+ /// <param name="positionTicks">The position ticks.</param>
+ public SeekGroupRequest(long positionTicks)
+ {
+ PositionTicks = positionTicks;
+ }
+
+ /// <summary>
+ /// Gets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Seek;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs
new file mode 100644
index 0000000000..44df127a6f
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs
@@ -0,0 +1,39 @@
+#nullable disable
+
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class SetPlaylistItemGroupRequest.
+ /// </summary>
+ public class SetPlaylistItemGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SetPlaylistItemGroupRequest"/> class.
+ /// </summary>
+ /// <param name="playlistItemId">The playlist identifier of the item.</param>
+ public SetPlaylistItemGroupRequest(Guid playlistItemId)
+ {
+ PlaylistItemId = playlistItemId;
+ }
+
+ /// <summary>
+ /// Gets the playlist identifier of the playing item.
+ /// </summary>
+ /// <value>The playlist identifier of the playing item.</value>
+ public Guid PlaylistItemId { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.SetPlaylistItem;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs
new file mode 100644
index 0000000000..d250eab56e
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs
@@ -0,0 +1,38 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class SetRepeatModeGroupRequest.
+ /// </summary>
+ public class SetRepeatModeGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SetRepeatModeGroupRequest"/> class.
+ /// </summary>
+ /// <param name="mode">The repeat mode.</param>
+ public SetRepeatModeGroupRequest(GroupRepeatMode mode)
+ {
+ Mode = mode;
+ }
+
+ /// <summary>
+ /// Gets the repeat mode.
+ /// </summary>
+ /// <value>The repeat mode.</value>
+ public GroupRepeatMode Mode { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.SetRepeatMode;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs
new file mode 100644
index 0000000000..5034e992eb
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs
@@ -0,0 +1,38 @@
+#nullable disable
+
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class SetShuffleModeGroupRequest.
+ /// </summary>
+ public class SetShuffleModeGroupRequest : AbstractPlaybackRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SetShuffleModeGroupRequest"/> class.
+ /// </summary>
+ /// <param name="mode">The shuffle mode.</param>
+ public SetShuffleModeGroupRequest(GroupShuffleMode mode)
+ {
+ Mode = mode;
+ }
+
+ /// <summary>
+ /// Gets the shuffle mode.
+ /// </summary>
+ /// <value>The shuffle mode.</value>
+ public GroupShuffleMode Mode { get; }
+
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.SetShuffleMode;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/StopGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/StopGroupRequest.cs
new file mode 100644
index 0000000000..ad739213c5
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/StopGroupRequest.cs
@@ -0,0 +1,21 @@
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class StopGroupRequest.
+ /// </summary>
+ public class StopGroupRequest : AbstractPlaybackRequest
+ {
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Stop;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/UnpauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/UnpauseGroupRequest.cs
new file mode 100644
index 0000000000..aaf3d65a84
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/UnpauseGroupRequest.cs
@@ -0,0 +1,21 @@
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
+{
+ /// <summary>
+ /// Class UnpauseGroupRequest.
+ /// </summary>
+ public class UnpauseGroupRequest : AbstractPlaybackRequest
+ {
+ /// <inheritdoc />
+ public override PlaybackRequestType Action { get; } = PlaybackRequestType.Unpause;
+
+ /// <inheritdoc />
+ public override void Apply(IGroupStateContext context, IGroupState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(this, context, state.Type, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
new file mode 100644
index 0000000000..b8ae9f3ff6
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
@@ -0,0 +1,579 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.Queue
+{
+ /// <summary>
+ /// Class PlayQueueManager.
+ /// </summary>
+ public class PlayQueueManager
+ {
+ /// <summary>
+ /// Placeholder index for when no item is playing.
+ /// </summary>
+ /// <value>The no-playing item index.</value>
+ private const int NoPlayingItemIndex = -1;
+
+ /// <summary>
+ /// Random number generator used to shuffle lists.
+ /// </summary>
+ /// <value>The random number generator.</value>
+ private readonly Random _randomNumberGenerator = new Random();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlayQueueManager" /> class.
+ /// </summary>
+ public PlayQueueManager()
+ {
+ Reset();
+ }
+
+ /// <summary>
+ /// Gets the playing item index.
+ /// </summary>
+ /// <value>The playing item index.</value>
+ public int PlayingItemIndex { get; private set; }
+
+ /// <summary>
+ /// Gets the last time the queue has been changed.
+ /// </summary>
+ /// <value>The last time the queue has been changed.</value>
+ public DateTime LastChange { get; private set; }
+
+ /// <summary>
+ /// Gets the shuffle mode.
+ /// </summary>
+ /// <value>The shuffle mode.</value>
+ public GroupShuffleMode ShuffleMode { get; private set; } = GroupShuffleMode.Sorted;
+
+ /// <summary>
+ /// Gets the repeat mode.
+ /// </summary>
+ /// <value>The repeat mode.</value>
+ public GroupRepeatMode RepeatMode { get; private set; } = GroupRepeatMode.RepeatNone;
+
+ /// <summary>
+ /// Gets or sets the sorted playlist.
+ /// </summary>
+ /// <value>The sorted playlist, or play queue of the group.</value>
+ private List<QueueItem> SortedPlaylist { get; set; } = new List<QueueItem>();
+
+ /// <summary>
+ /// Gets or sets the shuffled playlist.
+ /// </summary>
+ /// <value>The shuffled playlist, or play queue of the group.</value>
+ private List<QueueItem> ShuffledPlaylist { get; set; } = new List<QueueItem>();
+
+ /// <summary>
+ /// Checks if an item is playing.
+ /// </summary>
+ /// <returns><c>true</c> if an item is playing; <c>false</c> otherwise.</returns>
+ public bool IsItemPlaying()
+ {
+ return PlayingItemIndex != NoPlayingItemIndex;
+ }
+
+ /// <summary>
+ /// Gets the current playlist considering the shuffle mode.
+ /// </summary>
+ /// <returns>The playlist.</returns>
+ public IReadOnlyList<QueueItem> GetPlaylist()
+ {
+ return GetPlaylistInternal();
+ }
+
+ /// <summary>
+ /// Sets a new playlist. Playing item is reset.
+ /// </summary>
+ /// <param name="items">The new items of the playlist.</param>
+ public void SetPlaylist(IReadOnlyList<Guid> items)
+ {
+ SortedPlaylist.Clear();
+ ShuffledPlaylist.Clear();
+
+ SortedPlaylist = CreateQueueItemsFromArray(items);
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
+ Shuffle(ShuffledPlaylist);
+ }
+
+ PlayingItemIndex = NoPlayingItemIndex;
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Appends new items to the playlist. The specified order is mantained.
+ /// </summary>
+ /// <param name="items">The items to add to the playlist.</param>
+ public void Queue(IReadOnlyList<Guid> items)
+ {
+ var newItems = CreateQueueItemsFromArray(items);
+
+ SortedPlaylist.AddRange(newItems);
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ ShuffledPlaylist.AddRange(newItems);
+ }
+
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Shuffles the playlist. Shuffle mode is changed. The playlist gets re-shuffled if already shuffled.
+ /// </summary>
+ public void ShufflePlaylist()
+ {
+ if (PlayingItemIndex == NoPlayingItemIndex)
+ {
+ ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
+ Shuffle(ShuffledPlaylist);
+ }
+ else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
+ {
+ // First time shuffle.
+ var playingItem = SortedPlaylist[PlayingItemIndex];
+ ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
+ ShuffledPlaylist.RemoveAt(PlayingItemIndex);
+ Shuffle(ShuffledPlaylist);
+ ShuffledPlaylist.Insert(0, playingItem);
+ PlayingItemIndex = 0;
+ }
+ else
+ {
+ // Re-shuffle playlist.
+ var playingItem = ShuffledPlaylist[PlayingItemIndex];
+ ShuffledPlaylist.RemoveAt(PlayingItemIndex);
+ Shuffle(ShuffledPlaylist);
+ ShuffledPlaylist.Insert(0, playingItem);
+ PlayingItemIndex = 0;
+ }
+
+ ShuffleMode = GroupShuffleMode.Shuffle;
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Resets the playlist to sorted mode. Shuffle mode is changed.
+ /// </summary>
+ public void RestoreSortedPlaylist()
+ {
+ if (PlayingItemIndex != NoPlayingItemIndex)
+ {
+ var playingItem = ShuffledPlaylist[PlayingItemIndex];
+ PlayingItemIndex = SortedPlaylist.IndexOf(playingItem);
+ }
+
+ ShuffledPlaylist.Clear();
+
+ ShuffleMode = GroupShuffleMode.Sorted;
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Clears the playlist. Shuffle mode is preserved.
+ /// </summary>
+ /// <param name="clearPlayingItem">Whether to remove the playing item as well.</param>
+ public void ClearPlaylist(bool clearPlayingItem)
+ {
+ var playingItem = GetPlayingItem();
+ SortedPlaylist.Clear();
+ ShuffledPlaylist.Clear();
+ LastChange = DateTime.UtcNow;
+
+ if (!clearPlayingItem && playingItem != null)
+ {
+ SortedPlaylist.Add(playingItem);
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ ShuffledPlaylist.Add(playingItem);
+ }
+
+ PlayingItemIndex = 0;
+ }
+ else
+ {
+ PlayingItemIndex = NoPlayingItemIndex;
+ }
+ }
+
+ /// <summary>
+ /// Adds new items to the playlist right after the playing item. The specified order is mantained.
+ /// </summary>
+ /// <param name="items">The items to add to the playlist.</param>
+ public void QueueNext(IReadOnlyList<Guid> items)
+ {
+ var newItems = CreateQueueItemsFromArray(items);
+
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ var playingItem = GetPlayingItem();
+ var sortedPlayingItemIndex = SortedPlaylist.IndexOf(playingItem);
+ // Append items to sorted and shuffled playlist as they are.
+ SortedPlaylist.InsertRange(sortedPlayingItemIndex + 1, newItems);
+ ShuffledPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
+ }
+ else
+ {
+ SortedPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
+ }
+
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Gets playlist identifier of the playing item, if any.
+ /// </summary>
+ /// <returns>The playlist identifier of the playing item.</returns>
+ public Guid GetPlayingItemPlaylistId()
+ {
+ var playingItem = GetPlayingItem();
+ return playingItem?.PlaylistItemId ?? Guid.Empty;
+ }
+
+ /// <summary>
+ /// Gets the playing item identifier, if any.
+ /// </summary>
+ /// <returns>The playing item identifier.</returns>
+ public Guid GetPlayingItemId()
+ {
+ var playingItem = GetPlayingItem();
+ return playingItem?.ItemId ?? Guid.Empty;
+ }
+
+ /// <summary>
+ /// Sets the playing item using its identifier. If not in the playlist, the playing item is reset.
+ /// </summary>
+ /// <param name="itemId">The new playing item identifier.</param>
+ public void SetPlayingItemById(Guid itemId)
+ {
+ var playlist = GetPlaylistInternal();
+ PlayingItemIndex = playlist.FindIndex(item => item.ItemId.Equals(itemId));
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Sets the playing item using its playlist identifier. If not in the playlist, the playing item is reset.
+ /// </summary>
+ /// <param name="playlistItemId">The new playing item identifier.</param>
+ /// <returns><c>true</c> if playing item has been set; <c>false</c> if item is not in the playlist.</returns>
+ public bool SetPlayingItemByPlaylistId(Guid playlistItemId)
+ {
+ var playlist = GetPlaylistInternal();
+ PlayingItemIndex = playlist.FindIndex(item => item.PlaylistItemId.Equals(playlistItemId));
+ LastChange = DateTime.UtcNow;
+
+ return PlayingItemIndex != NoPlayingItemIndex;
+ }
+
+ /// <summary>
+ /// Sets the playing item using its position. If not in range, the playing item is reset.
+ /// </summary>
+ /// <param name="playlistIndex">The new playing item index.</param>
+ public void SetPlayingItemByIndex(int playlistIndex)
+ {
+ var playlist = GetPlaylistInternal();
+ if (playlistIndex < 0 || playlistIndex > playlist.Count)
+ {
+ PlayingItemIndex = NoPlayingItemIndex;
+ }
+ else
+ {
+ PlayingItemIndex = playlistIndex;
+ }
+
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Removes items from the playlist. If not removed, the playing item is preserved.
+ /// </summary>
+ /// <param name="playlistItemIds">The items to remove.</param>
+ /// <returns><c>true</c> if playing item got removed; <c>false</c> otherwise.</returns>
+ public bool RemoveFromPlaylist(IReadOnlyList<Guid> playlistItemIds)
+ {
+ var playingItem = GetPlayingItem();
+
+ SortedPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
+ ShuffledPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
+
+ LastChange = DateTime.UtcNow;
+
+ if (playingItem != null)
+ {
+ if (playlistItemIds.Contains(playingItem.PlaylistItemId))
+ {
+ // Playing item has been removed, picking previous item.
+ PlayingItemIndex--;
+ if (PlayingItemIndex < 0)
+ {
+ // Was first element, picking next if available.
+ // Default to no playing item otherwise.
+ PlayingItemIndex = SortedPlaylist.Count > 0 ? 0 : NoPlayingItemIndex;
+ }
+
+ return true;
+ }
+ else
+ {
+ // Restoring playing item.
+ SetPlayingItemByPlaylistId(playingItem.PlaylistItemId);
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Moves an item in the playlist to another position.
+ /// </summary>
+ /// <param name="playlistItemId">The item to move.</param>
+ /// <param name="newIndex">The new position.</param>
+ /// <returns><c>true</c> if the item has been moved; <c>false</c> otherwise.</returns>
+ public bool MovePlaylistItem(Guid playlistItemId, int newIndex)
+ {
+ var playlist = GetPlaylistInternal();
+ var playingItem = GetPlayingItem();
+
+ var oldIndex = playlist.FindIndex(item => item.PlaylistItemId.Equals(playlistItemId));
+ if (oldIndex < 0)
+ {
+ return false;
+ }
+
+ var queueItem = playlist[oldIndex];
+ playlist.RemoveAt(oldIndex);
+ newIndex = Math.Clamp(newIndex, 0, playlist.Count);
+ playlist.Insert(newIndex, queueItem);
+
+ LastChange = DateTime.UtcNow;
+ PlayingItemIndex = playlist.IndexOf(playingItem);
+ return true;
+ }
+
+ /// <summary>
+ /// Resets the playlist to its initial state.
+ /// </summary>
+ public void Reset()
+ {
+ SortedPlaylist.Clear();
+ ShuffledPlaylist.Clear();
+ PlayingItemIndex = NoPlayingItemIndex;
+ ShuffleMode = GroupShuffleMode.Sorted;
+ RepeatMode = GroupRepeatMode.RepeatNone;
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Sets the repeat mode.
+ /// </summary>
+ /// <param name="mode">The new mode.</param>
+ public void SetRepeatMode(GroupRepeatMode mode)
+ {
+ RepeatMode = mode;
+ LastChange = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Sets the shuffle mode.
+ /// </summary>
+ /// <param name="mode">The new mode.</param>
+ public void SetShuffleMode(GroupShuffleMode mode)
+ {
+ if (mode.Equals(GroupShuffleMode.Shuffle))
+ {
+ ShufflePlaylist();
+ }
+ else
+ {
+ RestoreSortedPlaylist();
+ }
+ }
+
+ /// <summary>
+ /// Toggles the shuffle mode between sorted and shuffled.
+ /// </summary>
+ public void ToggleShuffleMode()
+ {
+ if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
+ {
+ ShufflePlaylist();
+ }
+ else
+ {
+ RestoreSortedPlaylist();
+ }
+ }
+
+ /// <summary>
+ /// Gets the next item in the playlist considering repeat mode and shuffle mode.
+ /// </summary>
+ /// <returns>The next item in the playlist.</returns>
+ public QueueItem GetNextItemPlaylistId()
+ {
+ int newIndex;
+ var playlist = GetPlaylistInternal();
+
+ switch (RepeatMode)
+ {
+ case GroupRepeatMode.RepeatOne:
+ newIndex = PlayingItemIndex;
+ break;
+ case GroupRepeatMode.RepeatAll:
+ newIndex = PlayingItemIndex + 1;
+ if (newIndex >= playlist.Count)
+ {
+ newIndex = 0;
+ }
+
+ break;
+ default:
+ newIndex = PlayingItemIndex + 1;
+ break;
+ }
+
+ if (newIndex < 0 || newIndex >= playlist.Count)
+ {
+ return null;
+ }
+
+ return playlist[newIndex];
+ }
+
+ /// <summary>
+ /// Sets the next item in the queue as playing item.
+ /// </summary>
+ /// <returns><c>true</c> if the playing item changed; <c>false</c> otherwise.</returns>
+ public bool Next()
+ {
+ if (RepeatMode.Equals(GroupRepeatMode.RepeatOne))
+ {
+ LastChange = DateTime.UtcNow;
+ return true;
+ }
+
+ PlayingItemIndex++;
+ if (PlayingItemIndex >= SortedPlaylist.Count)
+ {
+ if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
+ {
+ PlayingItemIndex = 0;
+ }
+ else
+ {
+ PlayingItemIndex = SortedPlaylist.Count - 1;
+ return false;
+ }
+ }
+
+ LastChange = DateTime.UtcNow;
+ return true;
+ }
+
+ /// <summary>
+ /// Sets the previous item in the queue as playing item.
+ /// </summary>
+ /// <returns><c>true</c> if the playing item changed; <c>false</c> otherwise.</returns>
+ public bool Previous()
+ {
+ if (RepeatMode.Equals(GroupRepeatMode.RepeatOne))
+ {
+ LastChange = DateTime.UtcNow;
+ return true;
+ }
+
+ PlayingItemIndex--;
+ if (PlayingItemIndex < 0)
+ {
+ if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
+ {
+ PlayingItemIndex = SortedPlaylist.Count - 1;
+ }
+ else
+ {
+ PlayingItemIndex = 0;
+ return false;
+ }
+ }
+
+ LastChange = DateTime.UtcNow;
+ return true;
+ }
+
+ /// <summary>
+ /// Shuffles a given list.
+ /// </summary>
+ /// <param name="list">The list to shuffle.</param>
+ private void Shuffle<T>(IList<T> list)
+ {
+ int n = list.Count;
+ while (n > 1)
+ {
+ n--;
+ int k = _randomNumberGenerator.Next(n + 1);
+ T value = list[k];
+ list[k] = list[n];
+ list[n] = value;
+ }
+ }
+
+ /// <summary>
+ /// Creates a list from the array of items. Each item is given an unique playlist identifier.
+ /// </summary>
+ /// <returns>The list of queue items.</returns>
+ private List<QueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items)
+ {
+ var list = new List<QueueItem>();
+ foreach (var item in items)
+ {
+ var queueItem = new QueueItem(item);
+ list.Add(queueItem);
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Gets the current playlist considering the shuffle mode.
+ /// </summary>
+ /// <returns>The playlist.</returns>
+ private List<QueueItem> GetPlaylistInternal()
+ {
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ return ShuffledPlaylist;
+ }
+ else
+ {
+ return SortedPlaylist;
+ }
+ }
+
+ /// <summary>
+ /// Gets the current playing item, depending on the shuffle mode.
+ /// </summary>
+ /// <returns>The playing item.</returns>
+ private QueueItem GetPlayingItem()
+ {
+ if (PlayingItemIndex == NoPlayingItemIndex)
+ {
+ return null;
+ }
+ else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ return ShuffledPlaylist[PlayingItemIndex];
+ }
+ else
+ {
+ return SortedPlaylist[PlayingItemIndex];
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/Requests/JoinGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/Requests/JoinGroupRequest.cs
new file mode 100644
index 0000000000..38c9e8e20c
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/Requests/JoinGroupRequest.cs
@@ -0,0 +1,29 @@
+using System;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.Requests
+{
+ /// <summary>
+ /// Class JoinGroupRequest.
+ /// </summary>
+ public class JoinGroupRequest : ISyncPlayRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JoinGroupRequest"/> class.
+ /// </summary>
+ /// <param name="groupId">The identifier of the group to join.</param>
+ public JoinGroupRequest(Guid groupId)
+ {
+ GroupId = groupId;
+ }
+
+ /// <summary>
+ /// Gets the group identifier.
+ /// </summary>
+ /// <value>The identifier of the group to join.</value>
+ public Guid GroupId { get; }
+
+ /// <inheritdoc />
+ public RequestType Type { get; } = RequestType.JoinGroup;
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/Requests/LeaveGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/Requests/LeaveGroupRequest.cs
new file mode 100644
index 0000000000..545778264f
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/Requests/LeaveGroupRequest.cs
@@ -0,0 +1,13 @@
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.Requests
+{
+ /// <summary>
+ /// Class LeaveGroupRequest.
+ /// </summary>
+ public class LeaveGroupRequest : ISyncPlayRequest
+ {
+ /// <inheritdoc />
+ public RequestType Type { get; } = RequestType.LeaveGroup;
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/Requests/ListGroupsRequest.cs b/MediaBrowser.Controller/SyncPlay/Requests/ListGroupsRequest.cs
new file mode 100644
index 0000000000..4a234fdab5
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/Requests/ListGroupsRequest.cs
@@ -0,0 +1,13 @@
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.Requests
+{
+ /// <summary>
+ /// Class ListGroupsRequest.
+ /// </summary>
+ public class ListGroupsRequest : ISyncPlayRequest
+ {
+ /// <inheritdoc />
+ public RequestType Type { get; } = RequestType.ListGroups;
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/Requests/NewGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/Requests/NewGroupRequest.cs
new file mode 100644
index 0000000000..1321f0de8e
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/Requests/NewGroupRequest.cs
@@ -0,0 +1,28 @@
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay.Requests
+{
+ /// <summary>
+ /// Class NewGroupRequest.
+ /// </summary>
+ public class NewGroupRequest : ISyncPlayRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NewGroupRequest"/> class.
+ /// </summary>
+ /// <param name="groupName">The name of the new group.</param>
+ public NewGroupRequest(string groupName)
+ {
+ GroupName = groupName;
+ }
+
+ /// <summary>
+ /// Gets the group name.
+ /// </summary>
+ /// <value>The name of the new group.</value>
+ public string GroupName { get; }
+
+ /// <inheritdoc />
+ public RequestType Type { get; } = RequestType.NewGroup;
+ }
+}
diff --git a/MediaBrowser.Controller/TV/ITVSeriesManager.cs b/MediaBrowser.Controller/TV/ITVSeriesManager.cs
index 291dea04ef..e066c03fdc 100644
--- a/MediaBrowser.Controller/TV/ITVSeriesManager.cs
+++ b/MediaBrowser.Controller/TV/ITVSeriesManager.cs
@@ -6,16 +6,26 @@ using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.TV
{
+ /// <summary>
+ /// The TV Series manager.
+ /// </summary>
public interface ITVSeriesManager
{
/// <summary>
/// Gets the next up.
/// </summary>
+ /// <param name="query">The next up query.</param>
+ /// <param name="options">The dto options.</param>
+ /// <returns>The next up items.</returns>
QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options);
/// <summary>
/// Gets the next up.
/// </summary>
+ /// <param name="request">The next up request.</param>
+ /// <param name="parentsFolders">The list of parent folders.</param>
+ /// <param name="options">The dto options.</param>
+ /// <returns>The next up items.</returns>
QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options);
}
}