diff options
Diffstat (limited to 'MediaBrowser.Controller')
38 files changed, 677 insertions, 264 deletions
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs index 62eca3ea9..081f877f7 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationException.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs @@ -7,23 +7,29 @@ namespace MediaBrowser.Controller.Authentication /// </summary> public class AuthenticationException : Exception { - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> public AuthenticationException() : base() { - } - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> public AuthenticationException(string message) : base(message) { - } - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> + /// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param> public AuthenticationException(string message, Exception innerException) : base(message, innerException) { - } } } diff --git a/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs b/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs deleted file mode 100644 index 89d0be58f..000000000 --- a/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediaBrowser.Model.Devices; - -namespace MediaBrowser.Controller.Devices -{ - public class CameraImageUploadInfo - { - public LocalFileInfo FileInfo { get; set; } - public DeviceInfo Device { get; set; } - } -} diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 77d567631..ef3f43c75 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,6 +1,4 @@ using System; -using System.IO; -using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Events; @@ -12,11 +10,6 @@ namespace MediaBrowser.Controller.Devices public interface IDeviceManager { /// <summary> - /// Occurs when [camera image uploaded]. - /// </summary> - event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; - - /// <summary> /// Saves the capabilities. /// </summary> /// <param name="reportedId">The reported identifier.</param> @@ -46,22 +39,6 @@ namespace MediaBrowser.Controller.Devices QueryResult<DeviceInfo> GetDevices(DeviceQuery query); /// <summary> - /// Gets the upload history. - /// </summary> - /// <param name="deviceId">The device identifier.</param> - /// <returns>ContentUploadHistory.</returns> - ContentUploadHistory GetCameraUploadHistory(string deviceId); - - /// <summary> - /// Accepts the upload. - /// </summary> - /// <param name="deviceId">The device identifier.</param> - /// <param name="stream">The stream.</param> - /// <param name="file">The file.</param> - /// <returns>Task.</returns> - Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file); - - /// <summary> /// Determines whether this instance [can access device] the specified user identifier. /// </summary> bool CanAccessDevice(User user, string deviceId); diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 88e67b648..e09ccd204 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -44,6 +44,15 @@ namespace MediaBrowser.Controller.Drawing ImageDimensions GetImageSize(string path); /// <summary> + /// Gets the blurhash of an image. + /// </summary> + /// <param name="xComp">Amount of X components of DCT to take.</param> + /// <param name="yComp">Amount of Y components of DCT to take.</param> + /// <param name="path">The filepath of the image.</param> + /// <returns>The blurhash.</returns> + string GetImageBlurHash(int xComp, int yComp, string path); + + /// <summary> /// Encode an image. /// </summary> string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 79399807f..8800fdf99 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -41,13 +41,11 @@ namespace MediaBrowser.Controller.Drawing ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info); /// <summary> - /// Gets the dimensions of the image. + /// Gets the blurhash of the image. /// </summary> - /// <param name="item">The base item.</param> - /// <param name="info">The information.</param> - /// <param name="updateItem">Whether or not the item info should be updated.</param> - /// <returns>ImageDimensions</returns> - ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem); + /// <param name="path">Path to the image file.</param> + /// <returns>BlurHash</returns> + string GetImageBlurHash(string path); /// <summary> /// Gets the image cache tag. @@ -56,6 +54,7 @@ namespace MediaBrowser.Controller.Drawing /// <param name="image">The image.</param> /// <returns>Guid.</returns> string GetImageCacheTag(BaseItem item, ItemImageInfo image); + string GetImageCacheTag(BaseItem item, ChapterInfo info); /// <summary> diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index c216176e7..b702bdb71 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -97,14 +97,14 @@ namespace MediaBrowser.Controller.Entities.Audio list.Insert(0, albumArtist + "-" + Name); } - var id = this.GetProviderId(MetadataProviders.MusicBrainzAlbum); + var id = this.GetProviderId(MetadataProvider.MusicBrainzAlbum); if (!string.IsNullOrEmpty(id)) { list.Insert(0, "MusicAlbum-Musicbrainz-" + id); } - id = this.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); + id = this.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); if (!string.IsNullOrEmpty(id)) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 5e3056ccb..31136e004 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -128,7 +128,7 @@ namespace MediaBrowser.Controller.Entities.Audio private static List<string> GetUserDataKeys(MusicArtist item) { var list = new List<string>(); - var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist); + var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); if (!string.IsNullOrEmpty(id)) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 7ed8fa767..98c416bb7 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Controller.Entities Genres = Array.Empty<string>(); Studios = Array.Empty<string>(); ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - LockedFields = Array.Empty<MetadataFields>(); + LockedFields = Array.Empty<MetadataField>(); ImageInfos = Array.Empty<ItemImageInfo>(); ProductionLocations = Array.Empty<string>(); RemoteTrailers = Array.Empty<MediaUrl>(); @@ -557,7 +557,8 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// The logger /// </summary> - public static ILogger Logger { get; set; } + public static ILoggerFactory LoggerFactory { get; set; } + 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; } @@ -585,7 +586,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <value>The locked fields.</value> [JsonIgnore] - public MetadataFields[] LockedFields { get; set; } + public MetadataField[] LockedFields { get; set; } /// <summary> /// Gets the type of the media. @@ -1374,6 +1375,7 @@ namespace MediaBrowser.Controller.Entities new List<FileSystemMetadata>(); var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); + LibraryManager.UpdateImages(this); // ensure all image properties in DB are fresh if (ownedItemsChanged) { @@ -2222,6 +2224,7 @@ namespace MediaBrowser.Controller.Entities existingImage.DateModified = image.DateModified; existingImage.Width = image.Width; existingImage.Height = image.Height; + existingImage.BlurHash = image.BlurHash; } else { @@ -2373,6 +2376,46 @@ namespace MediaBrowser.Controller.Entities .ElementAtOrDefault(imageIndex); } + /// <summary> + /// Computes image index for given image or raises if no matching image found. + /// </summary> + /// <param name="image">Image to compute index for.</param> + /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found. + /// </exception> + /// <returns>Image index.</returns> + public int GetImageIndex(ItemImageInfo image) + { + if (image == null) + { + throw new ArgumentNullException(nameof(image)); + } + + if (image.Type == ImageType.Chapter) + { + var chapters = ItemRepository.GetChapters(this); + for (var i = 0; i < chapters.Count; i++) + { + if (chapters[i].ImagePath == image.Path) + { + return i; + } + } + + throw new ArgumentException("No chapter index found for image path", image.Path); + } + + var images = GetImages(image.Type).ToArray(); + for (var i = 0; i < images.Length; i++) + { + if (images[i].Path == image.Path) + { + return i; + } + } + + throw new ArgumentException("No image index found for image path", image.Path); + } + public IEnumerable<ItemImageInfo> GetImages(ImageType imageType) { if (imageType == ImageType.Chapter) diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index dcad2554b..c5ce001c0 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -11,6 +11,10 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Book; + public override bool SupportsPlayedStatus => true; + + public override bool SupportsPositionTicksResume => true; + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } @@ -20,6 +24,11 @@ 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/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index a468e0c35..29040f92e 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -341,6 +341,11 @@ namespace MediaBrowser.Controller.Entities { currentChild.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); } + else + { + // metadata is up-to-date; make sure DB has correct images dimensions and hash + LibraryManager.UpdateImages(currentChild); + } continue; } diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index fc46dec2e..12f5db2e0 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -28,6 +28,12 @@ namespace MediaBrowser.Controller.Entities public int Height { get; set; } + /// <summary> + /// Gets or sets the blurhash. + /// </summary> + /// <value>The blurhash.</value> + public string BlurHash { get; set; } + [JsonIgnore] public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 11dc472b6..d5c9ff025 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -173,7 +173,7 @@ namespace MediaBrowser.Controller.Entities.Movies { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 2475b2b7e..6f1c2501f 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -164,13 +164,13 @@ namespace MediaBrowser.Controller.Entities.TV { var list = base.GetUserDataKeys(); - var key = this.GetProviderId(MetadataProviders.Imdb); + var key = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); } - key = this.GetProviderId(MetadataProviders.Tvdb); + key = this.GetProviderId(MetadataProvider.Tvdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); @@ -493,7 +493,7 @@ namespace MediaBrowser.Controller.Entities.TV { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 0b8be90cd..7159b5b1a 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.Entities { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 4ce9ec6f8..86cff632c 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { @@ -66,7 +67,7 @@ namespace MediaBrowser.Controller.Entities parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; } - return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager) + return new UserViewBuilder(UserViewManager, LibraryManager, LoggerFactory.CreateLogger<UserViewBuilder>(), UserDataManager, TVSeriesManager, ConfigurationManager) .GetUserItems(parent, this, CollectionType, query); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 435a1e8da..db3e67f0d 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Entities { private readonly IUserViewManager _userViewManager; private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger<UserViewBuilder> _logger; private readonly IUserDataManager _userDataManager; private readonly ITVSeriesManager _tvSeriesManager; private readonly IServerConfigurationManager _config; @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Entities public UserViewBuilder( IUserViewManager userViewManager, ILibraryManager libraryManager, - ILogger logger, + ILogger<UserViewBuilder> logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config) @@ -611,7 +611,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasImdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Imdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Imdb)); if (hasValue != filterValue) { @@ -623,7 +623,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasTmdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Tmdb)); if (hasValue != filterValue) { @@ -635,7 +635,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasTvdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Tvdb)); if (hasValue != filterValue) { diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index c3ea7f347..72eb67a06 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -272,13 +272,13 @@ namespace MediaBrowser.Controller.Entities { if (ExtraType.HasValue) { - var key = this.GetProviderId(MetadataProviders.Tmdb); + var key = this.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, GetUserDataKey(key)); } - key = this.GetProviderId(MetadataProviders.Imdb); + key = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, GetUserDataKey(key)); @@ -286,13 +286,13 @@ namespace MediaBrowser.Controller.Entities } else { - var key = this.GetProviderId(MetadataProviders.Imdb); + var key = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); } - key = this.GetProviderId(MetadataProviders.Tmdb); + key = this.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index 4bbb60283..666a3f76b 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -35,7 +35,8 @@ namespace MediaBrowser.Controller.IO /// <param name="resolveShortcuts">if set to <c>true</c> [resolve shortcuts].</param> /// <returns>Dictionary{System.StringFileSystemInfo}.</returns> /// <exception cref="ArgumentNullException">path</exception> - public static FileSystemMetadata[] GetFilteredFileSystemEntries(IDirectoryService directoryService, + public static FileSystemMetadata[] GetFilteredFileSystemEntries( + IDirectoryService directoryService, string path, IFileSystem fileSystem, IServerApplicationHost appHost, diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 608ffc61c..d1d6c74b8 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -39,10 +39,9 @@ namespace MediaBrowser.Controller int HttpsPort { get; } /// <summary> - /// Gets a value indicating whether [supports HTTPS]. + /// Gets a value indicating whether the server should listen on an HTTPS port. /// </summary> - /// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value> - bool EnableHttps { get; } + bool ListenWithHttps { get; } /// <summary> /// Gets a value indicating whether this instance has update available. @@ -57,32 +56,52 @@ namespace MediaBrowser.Controller string FriendlyName { get; } /// <summary> - /// Gets the local ip address. + /// 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. /// </summary> - /// <value>The local ip address.</value> + /// <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); /// <summary> - /// Gets the local API URL. + /// 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. /// </summary> - /// <value>The local API URL.</value> + /// <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); /// <summary> - /// Gets the local API URL. + /// 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). /// </summary> - /// <param name="hostname">The hostname.</param> - /// <returns>The local API URL.</returns> - string GetLocalApiUrl(ReadOnlySpan<char> hostname); + /// <returns>The API URL.</returns> + string GetLoopbackHttpApiUrl(); /// <summary> - /// Gets the local API URL. + /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available. /// </summary> - /// <param name="address">The IP address.</param> - /// <returns>The local API URL.</returns> + /// <param name="address">The IP address to use as the hostname in the URL.</param> + /// <returns>The API URL.</returns> string GetLocalApiUrl(IPAddress address); /// <summary> + /// Gets a local (LAN) URL that can be used to access the API. + /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair. + /// </summary> + /// <param name="hostname">The hostname to use in the URL.</param> + /// <param name="scheme"> + /// The scheme to use for the URL. If null, the scheme will be selected automatically, + /// preferring HTTPS, if available. + /// </param> + /// <param name="port"> + /// The port to use for the URL. If null, the port will be selected automatically, + /// preferring the HTTPS port, if available. + /// </param> + /// <returns>The API URL.</returns> + string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null); + + /// <summary> /// Open a URL in an external browser window. /// </summary> /// <param name="url">The URL to open.</param> @@ -97,7 +116,5 @@ namespace MediaBrowser.Controller string ReverseVirtualPath(string path); Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next); - - Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next); } } diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 5d7c60910..c35a22ac7 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -71,7 +71,12 @@ namespace MediaBrowser.Controller string UserConfigurationDirectoryPath { get; } /// <summary> - /// Gets the internal metadata path. + /// Gets the default internal metadata path. + /// </summary> + string DefaultInternalMetadataPath { get; } + + /// <summary> + /// Gets the internal metadata path, either a custom path or the default. /// </summary> /// <value>The internal metadata path.</value> string InternalMetadataPath { get; } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 2e1c97f67..5afe356f4 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -27,14 +27,18 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="fileInfo">The file information.</param> /// <param name="parent">The parent.</param> + /// <param name="allowIgnorePath">Allow the path to be ignored.</param> /// <returns>BaseItem.</returns> - BaseItem ResolvePath(FileSystemMetadata fileInfo, - Folder parent = null); + BaseItem ResolvePath( + FileSystemMetadata fileInfo, + Folder parent = null, + bool allowIgnorePath = true); /// <summary> /// Resolves a set of files into a list of BaseItem /// </summary> - IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, + IEnumerable<BaseItem> ResolvePaths( + IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, @@ -118,7 +122,7 @@ namespace MediaBrowser.Controller.Library /// </summary> void QueueLibraryScan(); - void UpdateImages(BaseItem item); + void UpdateImages(BaseItem item, bool forceUpdate = false); /// <summary> /// Gets the default view. @@ -195,6 +199,7 @@ namespace MediaBrowser.Controller.Library /// Updates the item. /// </summary> void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); /// <summary> diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs index 46a97d181..0febef3d3 100644 --- a/MediaBrowser.Controller/Library/Profiler.cs +++ b/MediaBrowser.Controller/Library/Profiler.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Controller.Library /// <summary> /// The _logger /// </summary> - private readonly ILogger _logger; + private readonly ILogger<Profiler> _logger; /// <summary> /// Initializes a new instance of the <see cref="Profiler" /> class. diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 13df85aed..e89bc05aa 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -26,13 +26,13 @@ namespace MediaBrowser.Controller.LiveTv if (!IsSeries) { - var key = this.GetProviderId(MetadataProviders.Imdb); + var key = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); } - key = this.GetProviderId(MetadataProviders.Tmdb); + key = this.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); @@ -253,7 +253,7 @@ namespace MediaBrowser.Controller.LiveTv { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { if (IsMovie) diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs index cfec39b4e..1b8f41db6 100644 --- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs @@ -1,10 +1,19 @@ +#nullable enable +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.LiveTv { public class TimerEventInfo { - public string Id { get; set; } - public Guid ProgramId { get; set; } + public TimerEventInfo(string id) + { + Id = id; + } + + public string Id { get; } + + public Guid? ProgramId { get; set; } } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 662ab2535..73e966344 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Controller</PackageId> @@ -8,8 +13,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.5" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 8fefdd770..2d2f73148 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -459,7 +459,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); var outputVideoCodec = GetVideoEncoder(state, encodingOptions); - + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; if (!hasTextSubs) @@ -1338,7 +1338,7 @@ namespace MediaBrowser.Controller.MediaEncoding transcoderChannelLimit = 6; } - var isTranscodingAudio = !string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); + var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec); int? resultChannels = state.GetRequestedAudioChannels(codec); if (isTranscodingAudio) @@ -1734,7 +1734,8 @@ namespace MediaBrowser.Controller.MediaEncoding var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs) + if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs)) && width.HasValue && height.HasValue) { @@ -1991,7 +1992,7 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add("hwupload"); } - // When the input may or may not be hardware QSV decodable + // When the input may or may not be hardware QSV decodable else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { if (!hasTextSubs) @@ -2248,7 +2249,7 @@ namespace MediaBrowser.Controller.MediaEncoding flags.Add("+ignidx"); } - if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { flags.Add("+genpts"); } @@ -2511,7 +2512,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions) { - if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { return null; } @@ -2547,7 +2548,7 @@ namespace MediaBrowser.Controller.MediaEncoding encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); return null; } - return "-c:v h264_qsv "; + return "-c:v h264_qsv"; } break; case "hevc": @@ -2555,19 +2556,19 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { //return "-c:v hevc_qsv -load_plugin hevc_hw "; - return "-c:v hevc_qsv "; + return "-c:v hevc_qsv"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_qsv "; + return "-c:v mpeg2_qsv"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_qsv "; + return "-c:v vc1_qsv"; } break; } @@ -2587,32 +2588,32 @@ namespace MediaBrowser.Controller.MediaEncoding encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); return null; } - return "-c:v h264_cuvid "; + return "-c:v h264_cuvid"; } break; case "hevc": case "h265": if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { - return "-c:v hevc_cuvid "; + return "-c:v hevc_cuvid"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_cuvid "; + return "-c:v mpeg2_cuvid"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_cuvid "; + return "-c:v vc1_cuvid"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_cuvid "; + return "-c:v mpeg4_cuvid"; } break; } @@ -2626,38 +2627,38 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { - return "-c:v h264_mediacodec "; + return "-c:v h264_mediacodec"; } break; case "hevc": case "h265": if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { - return "-c:v hevc_mediacodec "; + return "-c:v hevc_mediacodec"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_mediacodec "; + return "-c:v mpeg2_mediacodec"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_mediacodec "; + return "-c:v mpeg4_mediacodec"; } break; case "vp8": if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vp8_mediacodec "; + return "-c:v vp8_mediacodec"; } break; case "vp9": if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vp9_mediacodec "; + return "-c:v vp9_mediacodec"; } break; } @@ -2671,25 +2672,25 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { - return "-c:v h264_mmal "; + return "-c:v h264_mmal"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_mmal "; + return "-c:v mpeg2_mmal"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_mmal "; + return "-c:v mpeg4_mmal"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_mmal "; + return "-c:v vc1_mmal"; } break; } @@ -2799,7 +2800,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -mpegts_m2ts_mode 1"; } - if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EncodingHelper.IsCopyCodec(videoCodec)) { if (state.VideoStream != null && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) @@ -2901,7 +2902,7 @@ namespace MediaBrowser.Controller.MediaEncoding var args = "-codec:a:0 " + codec; - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EncodingHelper.IsCopyCodec(codec)) { return args; } @@ -2973,5 +2974,10 @@ namespace MediaBrowser.Controller.MediaEncoding string.Empty, string.Empty).Trim(); } + + public static bool IsCopyCodec(string codec) + { + return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 1127a08de..7cd783595 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -302,7 +302,7 @@ namespace MediaBrowser.Controller.MediaEncoding return false; } - return BaseRequest.BreakOnNonKeyFrames && string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase); + return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec); } return false; @@ -367,7 +367,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { if (BaseRequest.Static - || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + || EncodingHelper.IsCopyCodec(OutputAudioCodec)) { if (AudioStream != null) { @@ -390,7 +390,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { if (BaseRequest.Static - || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + || EncodingHelper.IsCopyCodec(OutputAudioCodec)) { if (AudioStream != null) { @@ -409,7 +409,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.Level; } @@ -433,7 +433,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { if (BaseRequest.Static - || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.BitDepth; } @@ -451,7 +451,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { if (BaseRequest.Static - || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.RefFrames; } @@ -468,7 +468,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { if (BaseRequest.Static - || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream == null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate); } @@ -499,7 +499,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.PacketLength; } @@ -515,7 +515,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.Profile; } @@ -535,7 +535,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { if (BaseRequest.Static - || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.CodecTag; } @@ -549,7 +549,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { if (BaseRequest.Static - || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.IsAnamorphic; } @@ -562,7 +562,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.Codec; } @@ -575,7 +575,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EncodingHelper.IsCopyCodec(OutputAudioCodec)) { return AudioStream?.Codec; } @@ -589,7 +589,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { if (BaseRequest.Static - || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.IsInterlaced; } @@ -607,7 +607,7 @@ namespace MediaBrowser.Controller.MediaEncoding { get { - if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.IsAVC; } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index b710318ee..7dca793c6 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -40,9 +40,9 @@ namespace MediaBrowser.Controller.Net /// <summary> /// The logger /// </summary> - protected ILogger Logger; + protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger; - protected BasePeriodicWebSocketListener(ILogger logger) + protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger) { if (logger == null) { @@ -77,8 +77,6 @@ namespace MediaBrowser.Controller.Net return Task.CompletedTask; } - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// <summary> /// Starts sending messages over a web socket /// </summary> @@ -87,12 +85,12 @@ namespace MediaBrowser.Controller.Net { var vals = message.Data.Split(','); - var dueTimeMs = long.Parse(vals[0], UsCulture); - var periodMs = long.Parse(vals[1], UsCulture); + var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture); + var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture); var cancellationTokenSource = new CancellationTokenSource(); - Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); + Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name); var state = new TStateType { @@ -106,7 +104,7 @@ namespace MediaBrowser.Controller.Net } } - protected void SendData(bool force) + protected async Task SendData(bool force) { Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>[] tuples; @@ -130,13 +128,18 @@ namespace MediaBrowser.Controller.Net .ToArray(); } - foreach (var tuple in tuples) + IEnumerable<Task> GetTasks() { - SendData(tuple); + foreach (var tuple in tuples) + { + yield return SendData(tuple); + } } + + await Task.WhenAll(GetTasks()).ConfigureAwait(false); } - private async void SendData(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> tuple) + private async Task SendData(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> tuple) { var connection = tuple.Item1; @@ -150,12 +153,13 @@ namespace MediaBrowser.Controller.Net if (data != null) { - await connection.SendAsync(new WebSocketMessage<TReturnDataType> - { - MessageType = Name, - Data = data - - }, cancellationToken).ConfigureAwait(false); + await connection.SendAsync( + new WebSocketMessage<TReturnDataType> + { + MessageType = Name, + Data = data + }, + cancellationToken).ConfigureAwait(false); state.DateLastSendUtc = DateTime.UtcNow; } @@ -197,7 +201,7 @@ namespace MediaBrowser.Controller.Net /// <param name="connection">The connection.</param> private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection) { - Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); + Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name); // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really... // connection.Item1.Dispose(); @@ -242,6 +246,7 @@ namespace MediaBrowser.Controller.Net public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } } diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index 806478864..efb5f4ac3 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Events; using MediaBrowser.Model.Services; @@ -9,9 +8,9 @@ using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { /// <summary> - /// Interface IHttpServer + /// Interface IHttpServer. /// </summary> - public interface IHttpServer : IDisposable + public interface IHttpServer { /// <summary> /// Gets the URL prefix. @@ -20,11 +19,6 @@ namespace MediaBrowser.Controller.Net string[] UrlPrefixes { get; } /// <summary> - /// Stops this instance. - /// </summary> - void Stop(); - - /// <summary> /// Occurs when [web socket connected]. /// </summary> event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; @@ -40,22 +34,17 @@ namespace MediaBrowser.Controller.Net string GlobalResponse { get; set; } /// <summary> - /// Sends the http context to the socket listener + /// The HTTP request handler /// </summary> - /// <param name="ctx"></param> + /// <param name="context"></param> /// <returns></returns> - Task ProcessWebSocketRequest(HttpContext ctx); + Task RequestHandler(HttpContext context); /// <summary> - /// The HTTP request handler + /// Get the default CORS headers /// </summary> - /// <param name="httpReq"></param> - /// <param name="urlString"></param> - /// <param name="host"></param> - /// <param name="localPath"></param> - /// <param name="cancellationToken"></param> + /// <param name="req"></param> /// <returns></returns> - Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, - CancellationToken cancellationToken); + IDictionary<string, string> GetDefaultCorsHeaders(IRequest req); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 31eb7ccb7..3ef8e5f6d 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,4 +1,7 @@ +#nullable enable + using System; +using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; @@ -7,18 +10,12 @@ using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { - public interface IWebSocketConnection : IDisposable + public interface IWebSocketConnection { /// <summary> /// Occurs when [closed]. /// </summary> - event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets the id. - /// </summary> - /// <value>The id.</value> - Guid Id { get; } + event EventHandler<EventArgs>? Closed; /// <summary> /// Gets the last activity date. @@ -27,21 +24,22 @@ namespace MediaBrowser.Controller.Net DateTime LastActivityDate { get; } /// <summary> - /// Gets or sets the URL. + /// Gets or sets the date of last Keeplive received. /// </summary> - /// <value>The URL.</value> - string Url { get; set; } + /// <value>The date of last Keeplive received.</value> + DateTime LastKeepAliveDate { get; set; } + /// <summary> /// Gets or sets the query string. /// </summary> /// <value>The query string.</value> - IQueryCollection QueryString { get; set; } + IQueryCollection QueryString { get; } /// <summary> /// Gets or sets the receive action. /// </summary> /// <value>The receive action.</value> - Func<WebSocketMessageInfo, Task> OnReceive { get; set; } + Func<WebSocketMessageInfo, Task>? OnReceive { get; set; } /// <summary> /// Gets the state. @@ -53,7 +51,7 @@ namespace MediaBrowser.Controller.Net /// Gets the remote end point. /// </summary> /// <value>The remote end point.</value> - string RemoteEndPoint { get; } + IPAddress? RemoteEndPoint { get; } /// <summary> /// Sends a message asynchronously. @@ -65,21 +63,6 @@ namespace MediaBrowser.Controller.Net /// <exception cref="ArgumentNullException">message</exception> Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken); - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="buffer">The buffer.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendAsync(byte[] buffer, CancellationToken cancellationToken); - - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="text">The text.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">buffer</exception> - Task SendAsync(string text, CancellationToken cancellationToken); + Task ProcessAsync(CancellationToken cancellationToken = default); } } diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs index 3ccecf0eb..a5b94ea5e 100644 --- a/MediaBrowser.Controller/Net/SecurityException.cs +++ b/MediaBrowser.Controller/Net/SecurityException.cs @@ -2,20 +2,36 @@ using System; namespace MediaBrowser.Controller.Net { + /// <summary> + /// The exception that is thrown when a user is authenticated, but not authorized to access a requested resource. + /// </summary> public class SecurityException : Exception { + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + public SecurityException() + : base() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> public SecurityException(string message) : base(message) { - } - public SecurityExceptionType SecurityExceptionType { get; set; } - } - - public enum SecurityExceptionType - { - Unauthenticated = 0, - ParentalControl = 1 + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + /// <param name="message">The message that describes the error</param> + /// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param> + public SecurityException(string message, Exception innerException) + : base(message, innerException) + { + } } } diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index a59c96ac7..04450085b 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; @@ -20,6 +21,6 @@ namespace MediaBrowser.Controller.Session /// <summary> /// Sends the message. /// </summary> - Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken); + Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 771027103..4c2f834cb 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; namespace MediaBrowser.Controller.Session { @@ -141,6 +142,24 @@ namespace MediaBrowser.Controller.Session Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken); /// <summary> + /// Sends the SyncPlayCommand. + /// </summary> + /// <param name="sessionId">The session id.</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); + + /// <summary> + /// Sends the SyncPlayGroupUpdate. + /// </summary> + /// <param name="sessionId">The session id.</param> + /// <param name="command">The group update.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken); + + /// <summary> /// Sends the browse command. /// </summary> /// <param name="controllingSessionId">The controlling session identifier.</param> diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index f1f10a3a3..2ba7c9fec 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Session { /// <summary> - /// Class SessionInfo + /// Class SessionInfo. /// </summary> - public class SessionInfo : IDisposable + public sealed class SessionInfo : IDisposable { - private ISessionManager _sessionManager; + // 1 second + private const long ProgressIncrement = 10000000; + + private readonly ISessionManager _sessionManager; private readonly ILogger _logger; + + private readonly object _progressLock = new object(); + private Timer _progressTimer; + private PlaybackProgressInfo _lastProgressInfo; + + private bool _disposed = false; + public SessionInfo(ISessionManager sessionManager, ILogger logger) { _sessionManager = sessionManager; @@ -97,8 +107,6 @@ namespace MediaBrowser.Controller.Session /// <value>The name of the device.</value> public string DeviceName { get; set; } - public string DeviceType { get; set; } - /// <summary> /// Gets or sets the now playing item. /// </summary> @@ -128,22 +136,6 @@ namespace MediaBrowser.Controller.Session [JsonIgnore] public ISessionController[] SessionControllers { get; set; } - /// <summary> - /// Gets or sets the supported commands. - /// </summary> - /// <value>The supported commands.</value> - public string[] SupportedCommands - { - get - { - if (Capabilities == null) - { - return new string[] { }; - } - return Capabilities.SupportedCommands; - } - } - public TranscodingInfo TranscodingInfo { get; set; } /// <summary> @@ -215,6 +207,14 @@ namespace MediaBrowser.Controller.Session } } + public QueueItem[] NowPlayingQueue { get; set; } + + public bool HasCustomDeviceName { get; set; } + + public string PlaylistItemId { get; set; } + + public string UserPrimaryImageTag { get; set; } + public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory) { var controllers = SessionControllers.ToList(); @@ -258,10 +258,6 @@ namespace MediaBrowser.Controller.Session return false; } - private readonly object _progressLock = new object(); - private Timer _progressTimer; - private PlaybackProgressInfo _lastProgressInfo; - public void StartAutomaticProgress(PlaybackProgressInfo progressInfo) { if (_disposed) @@ -284,9 +280,6 @@ namespace MediaBrowser.Controller.Session } } - // 1 second - private const long ProgressIncrement = 10000000; - private async void OnProgressTimerCallback(object state) { if (_disposed) @@ -345,8 +338,7 @@ namespace MediaBrowser.Controller.Session } } - private bool _disposed = false; - + /// <inheritdoc /> public void Dispose() { _disposed = true; @@ -358,30 +350,12 @@ namespace MediaBrowser.Controller.Session foreach (var controller in controllers) { - var disposable = controller as IDisposable; - - if (disposable != null) + if (controller is IDisposable disposable) { _logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name); - - try - { - disposable.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing session controller"); - } + disposable.Dispose(); } } - - _sessionManager = null; } - - public QueueItem[] NowPlayingQueue { get; set; } - public bool HasCustomDeviceName { get; set; } - public string PlaylistItemId { get; set; } - public string ServerId { get; set; } - public string UserPrimaryImageTag { get; set; } } } diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs new file mode 100644 index 000000000..28a3ac505 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -0,0 +1,169 @@ +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> + /// Default ping value used for sessions. + /// </summary> + public long DefaulPing { 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 whether playback is paused. + /// </summary> + /// <value>Playback is paused.</value> + public bool IsPaused { get; set; } + + /// <summary> + /// Gets or sets the 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.ToString())) + { + return; + } + + var member = new GroupMember(); + member.Session = session; + member.Ping = DefaulPing; + member.IsBuffering = false; + Participants[session.Id.ToString()] = member; + } + + /// <summary> + /// Removes the session from the group. + /// </summary> + /// <param name="session">The session.</param> + public void RemoveSession(SessionInfo session) + { + if (!ContainsSession(session.Id.ToString())) + { + return; + } + + Participants.Remove(session.Id.ToString(), 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.ToString())) + { + return; + } + + Participants[session.Id.ToString()].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 = Int64.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.ToString())) + { + return; + } + + Participants[session.Id.ToString()].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 new file mode 100644 index 000000000..a3975c334 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Class GroupMember. + /// </summary> + public class GroupMember + { + /// <summary> + /// Gets or sets 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 the session. + /// </summary> + /// <value>The session.</value> + public SessionInfo Session { get; set; } + + /// <summary> + /// Gets or sets the ping. + /// </summary> + /// <value>The ping.</value> + public long Ping { get; set; } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs new file mode 100644 index 000000000..de1fcd259 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs @@ -0,0 +1,67 @@ +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 InitGroup(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(); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs new file mode 100644 index 000000000..006fb687b --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Interface ISyncPlayManager. + /// </summary> + public interface ISyncPlayManager + { + /// <summary> + /// Creates a new group. + /// </summary> + /// <param name="session">The session that's creating the group.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void NewGroup(SessionInfo session, 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); + + /// <summary> + /// Removes the session from a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void LeaveGroup(SessionInfo session, 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); + + /// <summary> + /// Handle a request by a session in a group. + /// </summary> + /// <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); + + /// <summary> + /// Maps a session to a group. + /// </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); + } +} |
