From 387fa474aa8ee8e237648ab0ea3130b8b35cf92f Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Thu, 2 Apr 2020 17:45:33 -0400 Subject: Document HTTPS configuration options --- .../Configuration/ServerConfiguration.cs | 35 +++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 3107ec2426..b14b347ccc 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -45,17 +45,24 @@ namespace MediaBrowser.Model.Configuration public int HttpsPortNumber { get; set; } /// - /// Gets or sets a value indicating whether [use HTTPS]. + /// Gets or sets a value indicating whether to use HTTPS. /// - /// true if [use HTTPS]; otherwise, false. + /// + /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be + /// provided for and . + /// public bool EnableHttps { get; set; } + public bool EnableNormalizedItemByNameIds { get; set; } /// - /// Gets or sets the value pointing to the file system where the ssl certificate is located.. + /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. /// - /// The value pointing to the file system where the ssl certificate is located.. public string CertificatePath { get; set; } + + /// + /// Gets or sets the password required to access the X.509 certificate data in the file specified by . + /// public string CertificatePassword { get; set; } /// @@ -65,8 +72,11 @@ namespace MediaBrowser.Model.Configuration public bool IsPortAuthorized { get; set; } public bool AutoRunWebApp { get; set; } + public bool EnableRemoteAccess { get; set; } + public bool CameraUploadUpgraded { get; set; } + public bool CollectionsUpgraded { get; set; } /// @@ -82,6 +92,7 @@ namespace MediaBrowser.Model.Configuration /// /// The metadata path. public string MetadataPath { get; set; } + public string MetadataNetworkPath { get; set; } /// @@ -204,15 +215,31 @@ namespace MediaBrowser.Model.Configuration public int RemoteClientBitrateLimit { get; set; } public bool EnableFolderView { get; set; } + public bool EnableGroupingIntoCollections { get; set; } + public bool DisplaySpecialsWithinSeasons { get; set; } + public string[] LocalNetworkSubnets { get; set; } + public string[] LocalNetworkAddresses { get; set; } + public string[] CodecsUsed { get; set; } + public bool IgnoreVirtualInterfaces { get; set; } + public bool EnableExternalContentInSuggestions { get; set; } + + /// + /// Gets or sets a value indicating whether the server should force connections over HTTPS. + /// public bool RequireHttps { get; set; } + + /// + /// Gets or sets a value indicating whether the server is behind a reverse proxy. + /// public bool IsBehindProxy { get; set; } + public bool EnableNewOmdbSupport { get; set; } public string[] RemoteIPFilter { get; set; } -- cgit v1.2.3 From c78413cf7cee018d50bce8e02d9cb7e3e5626d90 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Mon, 6 Apr 2020 00:03:56 -0400 Subject: Disable UPnP by default --- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 3107ec2426..9983aa0e09 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -254,7 +254,7 @@ namespace MediaBrowser.Model.Configuration AutoRunWebApp = true; EnableRemoteAccess = true; - EnableUPnP = true; + EnableUPnP = false; MinResumePct = 5; MaxResumePct = 90; -- cgit v1.2.3 From bcf1ef319a228dc6383b1730e8222c1b2294e697 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 17 Apr 2020 00:27:18 -0400 Subject: Update documentation for EnableUPnP --- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 9983aa0e09..b5e8d5589a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -15,9 +15,8 @@ namespace MediaBrowser.Model.Configuration private string _baseUrl; /// - /// Gets or sets a value indicating whether [enable u pn p]. + /// Gets or sets a value indicating whether to enable automatic port forwarding. /// - /// true if [enable u pn p]; otherwise, false. public bool EnableUPnP { get; set; } /// -- cgit v1.2.3 From 68c7a914c3acbd21a9ca879829bf6a670d4cf185 Mon Sep 17 00:00:00 2001 From: sparky8251 Date: Sun, 26 Apr 2020 11:28:17 -0400 Subject: Added option to disable metrics collection and defaulted it to off --- Emby.Server.Implementations/ApplicationHost.cs | 7 +++++ .../Emby.Server.Implementations.csproj | 1 + Jellyfin.Server/Jellyfin.Server.csproj | 1 - .../Routines/DisableMetricsCollection.cs | 33 ++++++++++++++++++++++ Jellyfin.Server/Program.cs | 4 --- Jellyfin.Server/Startup.cs | 11 ++++++-- .../Configuration/ServerConfiguration.cs | 6 ++++ 7 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 Jellyfin.Server/Migrations/Routines/DisableMetricsCollection.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 33aec1a06b..7e7b785d85 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -106,6 +106,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; +using Prometheus.DotNetRuntime; namespace Emby.Server.Implementations { @@ -259,6 +260,12 @@ namespace Emby.Server.Implementations _startupOptions = options; + // Initialize runtime stat collection + if (ServerConfigurationManager.Configuration.EnableMetrics) + { + IDisposable collector = DotNetRuntimeStatsBuilder.Default().StartCollecting(); + } + fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); _networkManager.NetworkChanged += OnNetworkChanged; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index bf4a0d939f..44fc932e39 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -39,6 +39,7 @@ + diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index c49fc41f46..88114d9994 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -45,7 +45,6 @@ - diff --git a/Jellyfin.Server/Migrations/Routines/DisableMetricsCollection.cs b/Jellyfin.Server/Migrations/Routines/DisableMetricsCollection.cs new file mode 100644 index 0000000000..b5dc43614e --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/DisableMetricsCollection.cs @@ -0,0 +1,33 @@ +using System; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// + /// Disable metrics collections for all installations since it can be a security risk if not properly secured. + /// + internal class DisableMetricsCollection : IMigrationRoutine + { + /// + public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}"); + + /// + public string Name => "DisableMetricsCollection"; + + /// + public void Perform(CoreAppHost host, ILogger logger) + { + // Set EnableMetrics to false since it can leak sensitive information if not properly secured + var metrics = host.ServerConfigurationManager.Configuration.EnableMetrics; + if (metrics) + { + logger.LogInformation("Disabling metrics collection during migration"); + metrics = false; + + host.ServerConfigurationManager.SaveConfiguration("false", metrics); + } + } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index be070f9d52..193d30e3a7 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -28,7 +28,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Prometheus.DotNetRuntime; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; @@ -162,9 +161,6 @@ namespace Jellyfin.Server ApplicationHost.LogEnvironmentInfo(_logger, appPaths); - // Initialize runtime stat collection - IDisposable collector = DotNetRuntimeStatsBuilder.Default().StartCollecting(); - // Make sure we have all the code pages we can get // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 2e5f843e3a..8f85161c7d 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -70,11 +70,18 @@ namespace Jellyfin.Server app.UseJellyfinApiSwagger(); app.UseRouting(); app.UseAuthorization(); - app.UseHttpMetrics(); // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad + if (_serverConfigurationManager.Configuration.EnableMetrics) + { + app.UseHttpMetrics(); // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad + } + app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapMetrics(); + if (_serverConfigurationManager.Configuration.EnableMetrics) + { + endpoints.MapMetrics(); + } }); app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index b5e8d5589a..063ccd9b9a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -19,6 +19,11 @@ namespace MediaBrowser.Model.Configuration /// public bool EnableUPnP { get; set; } + /// + /// Gets or sets a value indicating whether to enable prometheus metrics exporting. + /// + public bool EnableMetrics { get; set; } + /// /// Gets or sets the public mapped port. /// @@ -246,6 +251,7 @@ namespace MediaBrowser.Model.Configuration PublicHttpsPort = DefaultHttpsPort; HttpServerPortNumber = DefaultHttpPort; HttpsPortNumber = DefaultHttpsPort; + EnableMetrics = false; EnableHttps = false; EnableDashboardResponseCaching = true; EnableCaseSensitiveItemIds = true; -- cgit v1.2.3 From 57b5ec1d514ad8c5a0e8aced4b84b46b4c8923a7 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 26 Apr 2020 12:07:54 -0400 Subject: Remove unnecessary properties from SystemInfo response object These properties do not provide any useful information to the client. The client would already have to have all this information in order to connect to the endpoint to retrieve it --- Emby.Server.Implementations/ApplicationHost.cs | 4 ---- Jellyfin.Server/Program.cs | 3 --- .../Configuration/ServerConfiguration.cs | 5 ----- MediaBrowser.Model/System/SystemInfo.cs | 18 ------------------ 4 files changed, 30 deletions(-) (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 9655d9f5ee..edfed67a18 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -94,7 +94,6 @@ using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; @@ -1143,9 +1142,6 @@ namespace Emby.Server.Implementations ItemsByNamePath = ApplicationPaths.InternalMetadataPath, InternalMetadataPath = ApplicationPaths.InternalMetadataPath, CachePath = ApplicationPaths.CachePath, - HttpServerPortNumber = HttpPort, - SupportsHttps = ListenWithHttps || ServerConfigurationManager.Configuration.IsBehindProxy, - HttpsPortNumber = HttpsPort, OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystemDisplayName = OperatingSystem.Name, CanSelfRestart = CanSelfRestart, diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 7aa238efac..1c586ffdd6 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -10,14 +10,11 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using CommandLine; -using Emby.Drawing; using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; -using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Extensions; using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 6ee6a1f932..e4cd6c34ad 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -234,11 +234,6 @@ namespace MediaBrowser.Model.Configuration /// public bool RequireHttps { get; set; } - /// - /// Gets or sets a value indicating whether the server is behind a reverse proxy. - /// - public bool IsBehindProxy { get; set; } - public bool EnableNewOmdbSupport { get; set; } public string[] RemoteIPFilter { get; set; } diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 9753f4e06d..f2c5aa1e39 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -115,24 +115,6 @@ namespace MediaBrowser.Model.System /// The transcode path. public string TranscodingTempPath { get; set; } - /// - /// Gets or sets the HTTP server port number. - /// - /// The HTTP server port number. - public int HttpServerPortNumber { get; set; } - - /// - /// Gets or sets a value indicating whether a client can connect to the server over HTTPS, either directly or - /// via a reverse proxy. - /// - public bool SupportsHttps { get; set; } - - /// - /// Gets or sets the HTTPS server port number. - /// - /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } - /// /// Gets or sets a value indicating whether this instance has update available. /// -- cgit v1.2.3 From 459297211ecb435886e8cdde8a6521671ca869f6 Mon Sep 17 00:00:00 2001 From: gion Date: Sat, 4 Apr 2020 17:59:16 +0200 Subject: Implement syncplay permissions for a user --- .../Syncplay/SyncplayManager.cs | 41 ++++++++++++++++++++++ MediaBrowser.Model/Configuration/SyncplayAccess.cs | 23 ++++++++++++ MediaBrowser.Model/Users/UserPolicy.cs | 7 ++++ 3 files changed, 71 insertions(+) create mode 100644 MediaBrowser.Model/Configuration/SyncplayAccess.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs b/Emby.Server.Implementations/Syncplay/SyncplayManager.cs index f76d243d58..f6311d098f 100644 --- a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs +++ b/Emby.Server.Implementations/Syncplay/SyncplayManager.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Syncplay; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Syncplay; namespace Emby.Server.Implementations.Syncplay @@ -21,6 +22,11 @@ namespace Emby.Server.Implementations.Syncplay /// private readonly ILogger _logger; + /// + /// The user manager. + /// + private readonly IUserManager _userManager; + /// /// The session manager. /// @@ -42,9 +48,11 @@ namespace Emby.Server.Implementations.Syncplay public SyncplayManager( ILogger logger, + IUserManager userManager, ISessionManager sessionManager) { _logger = logger; + _userManager = userManager; _sessionManager = sessionManager; _sessionManager.SessionEnded += _sessionManager_SessionEnded; @@ -125,8 +133,16 @@ namespace Emby.Server.Implementations.Syncplay /// public void NewGroup(SessionInfo session) { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncplayAccess != SyncplayAccess.CreateAndJoinGroups) { + // TODO: shall an error message be sent back to the client? + return; + } + if (IsSessionInGroup(session)) + { LeaveGroup(session); } @@ -139,6 +155,14 @@ namespace Emby.Server.Implementations.Syncplay /// public void JoinGroup(SessionInfo session, string groupId) { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncplayAccess == SyncplayAccess.None) + { + // TODO: shall an error message be sent back to the client? + return; + } + if (IsSessionInGroup(session)) { if (GetSessionGroup(session).Equals(groupId)) return; @@ -163,6 +187,8 @@ namespace Emby.Server.Implementations.Syncplay /// public void LeaveGroup(SessionInfo session) { + // TODO: what happens to users that are in a group and get their permissions revoked? + ISyncplayController group; _sessionToGroupMap.TryGetValue(session.Id, out group); @@ -186,6 +212,13 @@ namespace Emby.Server.Implementations.Syncplay /// public List ListGroups(SessionInfo session) { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncplayAccess == SyncplayAccess.None) + { + return new List(); + } + // Filter by playing item if the user is viewing something already if (session.NowPlayingItem != null) { @@ -207,6 +240,14 @@ namespace Emby.Server.Implementations.Syncplay /// public void HandleRequest(SessionInfo session, SyncplayRequestInfo request) { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncplayAccess == SyncplayAccess.None) + { + // TODO: same as LeaveGroup + return; + } + ISyncplayController group; _sessionToGroupMap.TryGetValue(session.Id, out group); diff --git a/MediaBrowser.Model/Configuration/SyncplayAccess.cs b/MediaBrowser.Model/Configuration/SyncplayAccess.cs new file mode 100644 index 0000000000..cddf68c42d --- /dev/null +++ b/MediaBrowser.Model/Configuration/SyncplayAccess.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Model.Configuration +{ + /// + /// Enum SyncplayAccess. + /// + public enum SyncplayAccess + { + /// + /// User can create groups and join them. + /// + CreateAndJoinGroups, + + /// + /// User can only join already existing groups. + /// + JoinGroups, + + /// + /// Syncplay is disabled for the user. + /// + None + } +} diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index ae2b3fd4e9..cf576c3582 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -80,6 +80,12 @@ namespace MediaBrowser.Model.Users public string AuthenticationProviderId { get; set; } public string PasswordResetProviderId { get; set; } + /// + /// Gets or sets a value indicating what Syncplay features the user can access. + /// + /// Access level to Syncplay features. + public SyncplayAccess SyncplayAccess { get; set; } + public UserPolicy() { IsHidden = true; @@ -125,6 +131,7 @@ namespace MediaBrowser.Model.Users EnableContentDownloading = true; EnablePublicSharing = true; EnableRemoteAccess = true; + SyncplayAccess = SyncplayAccess.CreateAndJoinGroups; } } } -- cgit v1.2.3 From 8a6ec2fb713cb77e91d2fceea8b4fce8e7d17395 Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 6 May 2020 23:42:53 +0200 Subject: Rename Syncplay to SyncPlay --- Emby.Server.Implementations/ApplicationHost.cs | 6 +- .../Session/SessionManager.cs | 10 +- .../SyncPlay/SyncPlayController.cs | 548 +++++++++++++++++++++ .../SyncPlay/SyncPlayManager.cs | 385 +++++++++++++++ .../Syncplay/SyncplayController.cs | 548 --------------------- .../Syncplay/SyncplayManager.cs | 385 --------------- MediaBrowser.Api/SyncPlay/SyncPlayService.cs | 312 ++++++++++++ MediaBrowser.Api/SyncPlay/TimeSyncService.cs | 52 ++ MediaBrowser.Api/Syncplay/SyncplayService.cs | 312 ------------ MediaBrowser.Api/Syncplay/TimeSyncService.cs | 52 -- MediaBrowser.Controller/Session/ISessionManager.cs | 10 +- MediaBrowser.Controller/SyncPlay/GroupInfo.cs | 150 ++++++ MediaBrowser.Controller/SyncPlay/GroupMember.cs | 28 ++ .../SyncPlay/ISyncPlayController.cs | 67 +++ .../SyncPlay/ISyncPlayManager.cs | 69 +++ MediaBrowser.Controller/Syncplay/GroupInfo.cs | 150 ------ MediaBrowser.Controller/Syncplay/GroupMember.cs | 28 -- .../Syncplay/ISyncplayController.cs | 67 --- .../Syncplay/ISyncplayManager.cs | 69 --- MediaBrowser.Model/Configuration/SyncplayAccess.cs | 6 +- MediaBrowser.Model/SyncPlay/GroupInfoView.cs | 38 ++ MediaBrowser.Model/SyncPlay/GroupUpdate.cs | 26 + MediaBrowser.Model/SyncPlay/GroupUpdateType.cs | 53 ++ MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs | 22 + MediaBrowser.Model/SyncPlay/PlaybackRequest.cs | 34 ++ MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs | 33 ++ MediaBrowser.Model/SyncPlay/SendCommand.cs | 38 ++ MediaBrowser.Model/SyncPlay/SendCommandType.cs | 21 + MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs | 20 + MediaBrowser.Model/Syncplay/GroupInfoView.cs | 38 -- MediaBrowser.Model/Syncplay/GroupUpdate.cs | 26 - MediaBrowser.Model/Syncplay/GroupUpdateType.cs | 53 -- MediaBrowser.Model/Syncplay/JoinGroupRequest.cs | 22 - MediaBrowser.Model/Syncplay/PlaybackRequest.cs | 34 -- MediaBrowser.Model/Syncplay/PlaybackRequestType.cs | 33 -- MediaBrowser.Model/Syncplay/SendCommand.cs | 38 -- MediaBrowser.Model/Syncplay/SendCommandType.cs | 21 - MediaBrowser.Model/Syncplay/UtcTimeResponse.cs | 20 - MediaBrowser.Model/Users/UserPolicy.cs | 8 +- 39 files changed, 1916 insertions(+), 1916 deletions(-) create mode 100644 Emby.Server.Implementations/SyncPlay/SyncPlayController.cs create mode 100644 Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs delete mode 100644 Emby.Server.Implementations/Syncplay/SyncplayController.cs delete mode 100644 Emby.Server.Implementations/Syncplay/SyncplayManager.cs create mode 100644 MediaBrowser.Api/SyncPlay/SyncPlayService.cs create mode 100644 MediaBrowser.Api/SyncPlay/TimeSyncService.cs delete mode 100644 MediaBrowser.Api/Syncplay/SyncplayService.cs delete mode 100644 MediaBrowser.Api/Syncplay/TimeSyncService.cs create mode 100644 MediaBrowser.Controller/SyncPlay/GroupInfo.cs create mode 100644 MediaBrowser.Controller/SyncPlay/GroupMember.cs create mode 100644 MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs create mode 100644 MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs delete mode 100644 MediaBrowser.Controller/Syncplay/GroupInfo.cs delete mode 100644 MediaBrowser.Controller/Syncplay/GroupMember.cs delete mode 100644 MediaBrowser.Controller/Syncplay/ISyncplayController.cs delete mode 100644 MediaBrowser.Controller/Syncplay/ISyncplayManager.cs create mode 100644 MediaBrowser.Model/SyncPlay/GroupInfoView.cs create mode 100644 MediaBrowser.Model/SyncPlay/GroupUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/GroupUpdateType.cs create mode 100644 MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs create mode 100644 MediaBrowser.Model/SyncPlay/PlaybackRequest.cs create mode 100644 MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs create mode 100644 MediaBrowser.Model/SyncPlay/SendCommand.cs create mode 100644 MediaBrowser.Model/SyncPlay/SendCommandType.cs create mode 100644 MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs delete mode 100644 MediaBrowser.Model/Syncplay/GroupInfoView.cs delete mode 100644 MediaBrowser.Model/Syncplay/GroupUpdate.cs delete mode 100644 MediaBrowser.Model/Syncplay/GroupUpdateType.cs delete mode 100644 MediaBrowser.Model/Syncplay/JoinGroupRequest.cs delete mode 100644 MediaBrowser.Model/Syncplay/PlaybackRequest.cs delete mode 100644 MediaBrowser.Model/Syncplay/PlaybackRequestType.cs delete mode 100644 MediaBrowser.Model/Syncplay/SendCommand.cs delete mode 100644 MediaBrowser.Model/Syncplay/SendCommandType.cs delete mode 100644 MediaBrowser.Model/Syncplay/UtcTimeResponse.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8419014c2c..730323c224 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -47,7 +47,7 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Emby.Server.Implementations.Syncplay; +using Emby.Server.Implementations.SyncPlay; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -81,7 +81,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.TV; -using MediaBrowser.Controller.Syncplay; +using MediaBrowser.Controller.SyncPlay; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.Model.Activity; @@ -645,7 +645,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6a64209c1a..aab745de4f 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -25,7 +25,7 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; -using MediaBrowser.Model.Syncplay; +using MediaBrowser.Model.SyncPlay; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session @@ -1156,19 +1156,19 @@ namespace Emby.Server.Implementations.Session } /// - public async Task SendSyncplayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken) + public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken) { CheckDisposed(); var session = GetSessionToRemoteControl(sessionId); - await SendMessageToSession(session, "SyncplayCommand", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false); } /// - public async Task SendSyncplayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken) + public async Task SendSyncPlayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken) { CheckDisposed(); var session = GetSessionToRemoteControl(sessionId); - await SendMessageToSession(session, "SyncplayGroupUpdate", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false); } private IEnumerable TranslateItemForPlayback(Guid id, User user) diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs new file mode 100644 index 0000000000..9c9758de1c --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -0,0 +1,548 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace Emby.Server.Implementations.SyncPlay +{ + /// + /// Class SyncPlayController. + /// + /// + /// Class is not thread-safe, external locking is required when accessing methods. + /// + public class SyncPlayController : ISyncPlayController, IDisposable + { + /// + /// Used to filter the sessions of a group. + /// + private enum BroadcastType + { + /// + /// All sessions will receive the message. + /// + AllGroup = 0, + /// + /// Only the specified session will receive the message. + /// + CurrentSession = 1, + /// + /// All sessions, except the current one, will receive the message. + /// + AllExceptCurrentSession = 2, + /// + /// Only sessions that are not buffering will receive the message. + /// + AllReady = 3 + } + + /// + /// The session manager. + /// + private readonly ISessionManager _sessionManager; + + /// + /// The SyncPlay manager. + /// + private readonly ISyncPlayManager _syncPlayManager; + + /// + /// The group to manage. + /// + private readonly GroupInfo _group = new GroupInfo(); + + /// + public Guid GetGroupId() => _group.GroupId; + + /// + public Guid GetPlayingItemId() => _group.PlayingItem.Id; + + /// + public bool IsGroupEmpty() => _group.IsEmpty(); + + private bool _disposed = false; + + public SyncPlayController( + ISessionManager sessionManager, + ISyncPlayManager syncPlayManager) + { + _sessionManager = sessionManager; + _syncPlayManager = syncPlayManager; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + // TODO: use this somewhere + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// Converts DateTime to UTC string. + /// + /// The date to convert. + /// The UTC string. + private string DateToUTCString(DateTime date) + { + return date.ToUniversalTime().ToString("o"); + } + + /// + /// Filters sessions of this group. + /// + /// The current session. + /// The filtering type. + /// The array of sessions matching the filter. + private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type) + { + switch (type) + { + case BroadcastType.CurrentSession: + return new SessionInfo[] { from }; + case BroadcastType.AllGroup: + return _group.Participants.Values.Select( + session => session.Session + ).ToArray(); + case BroadcastType.AllExceptCurrentSession: + return _group.Participants.Values.Select( + session => session.Session + ).Where( + session => !session.Id.Equals(from.Id) + ).ToArray(); + case BroadcastType.AllReady: + return _group.Participants.Values.Where( + session => !session.IsBuffering + ).Select( + session => session.Session + ).ToArray(); + default: + return new SessionInfo[] { }; + } + } + + /// + /// Sends a GroupUpdate message to the interested sessions. + /// + /// The current session. + /// The filtering type. + /// The message to send. + /// The cancellation token. + /// The task. + private Task SendGroupUpdate(SessionInfo from, BroadcastType type, GroupUpdate message, CancellationToken cancellationToken) + { + IEnumerable GetTasks() + { + SessionInfo[] sessions = FilterSessions(from, type); + foreach (var session in sessions) + { + yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken); + } + } + + return Task.WhenAll(GetTasks()); + } + + /// + /// Sends a playback command to the interested sessions. + /// + /// The current session. + /// The filtering type. + /// The message to send. + /// The cancellation token. + /// The task. + private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken) + { + IEnumerable GetTasks() + { + SessionInfo[] sessions = FilterSessions(from, type); + foreach (var session in sessions) + { + yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken); + } + } + + return Task.WhenAll(GetTasks()); + } + + /// + /// Builds a new playback command with some default values. + /// + /// The command type. + /// The SendCommand. + private SendCommand NewSyncPlayCommand(SendCommandType type) + { + return new SendCommand() + { + GroupId = _group.GroupId.ToString(), + Command = type, + PositionTicks = _group.PositionTicks, + When = DateToUTCString(_group.LastActivity), + EmittedAt = DateToUTCString(DateTime.UtcNow) + }; + } + + /// + /// Builds a new group update message. + /// + /// The update type. + /// The data to send. + /// The GroupUpdate. + private GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data) + { + return new GroupUpdate() + { + GroupId = _group.GroupId.ToString(), + Type = type, + Data = data + }; + } + + /// + public void InitGroup(SessionInfo session, CancellationToken cancellationToken) + { + _group.AddSession(session); + _syncPlayManager.AddSessionToGroup(session, this); + + _group.PlayingItem = session.FullNowPlayingItem; + _group.IsPaused = true; + _group.PositionTicks = session.PlayState.PositionTicks ?? 0; + _group.LastActivity = DateTime.UtcNow; + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); + } + + /// + public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) + { + if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id) + { + _group.AddSession(session); + _syncPlayManager.AddSessionToGroup(session, this); + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + + // Client join and play, syncing will happen client side + if (!_group.IsPaused) + { + var playCommand = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken); + } + else + { + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); + } + } + else + { + var playRequest = new PlayRequest(); + playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id }; + playRequest.StartPositionTicks = _group.PositionTicks; + var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); + SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); + } + } + + /// + public void SessionLeave(SessionInfo session, CancellationToken cancellationToken) + { + _group.RemoveSession(session); + _syncPlayManager.RemoveSessionFromGroup(session, this); + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + } + + /// + public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + // The server's job is to mantain a consistent state to which clients refer to, + // as also to notify clients of state changes. + // The actual syncing of media playback happens client side. + // Clients are aware of the server's time and use it to sync. + switch (request.Type) + { + case PlaybackRequestType.Play: + HandlePlayRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Pause: + HandlePauseRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Seek: + HandleSeekRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Buffering: + HandleBufferingRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.BufferingDone: + HandleBufferingDoneRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.UpdatePing: + HandlePingUpdateRequest(session, request); + break; + } + } + + /// + /// Handles a play action requested by a session. + /// + /// The session. + /// The play action. + /// The cancellation token. + private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (_group.IsPaused) + { + // Pick a suitable time that accounts for latency + var delay = _group.GetHighestPing() * 2; + delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + + // 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 + _group.IsPaused = false; + _group.LastActivity = DateTime.UtcNow.AddMilliseconds( + delay + ); + + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// + /// Handles a pause action requested by a session. + /// + /// The session. + /// The pause action. + /// The cancellation token. + private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (!_group.IsPaused) + { + // Pause group and compute the media playback position + _group.IsPaused = true; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - _group.LastActivity; + _group.LastActivity = currentTime; + // Seek only if playback actually started + // (a pause request may be issued during the delay added to account for latency) + _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// + /// Handles a seek action requested by a session. + /// + /// The session. + /// The seek action. + /// The cancellation token. + private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + // Sanitize PositionTicks + var ticks = SanitizePositionTicks(request.PositionTicks); + + // Pause and seek + _group.IsPaused = true; + _group.PositionTicks = ticks; + _group.LastActivity = DateTime.UtcNow; + + var command = NewSyncPlayCommand(SendCommandType.Seek); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + + /// + /// Handles a buffering action requested by a session. + /// + /// The session. + /// The buffering action. + /// The cancellation token. + private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (!_group.IsPaused) + { + // Pause group and compute the media playback position + _group.IsPaused = true; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - _group.LastActivity; + _group.LastActivity = currentTime; + _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + _group.SetBuffering(session, true); + + // Send pause command to all non-buffering sessions + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.AllReady, command, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// + /// Handles a buffering-done action requested by a session. + /// + /// The session. + /// The buffering-done action. + /// The cancellation token. + private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (_group.IsPaused) + { + _group.SetBuffering(session, false); + + var requestTicks = SanitizePositionTicks(request.PositionTicks); + + var when = request.When ?? DateTime.UtcNow; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - when; + var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; + var delay = _group.PositionTicks - clientPosition.Ticks; + + if (_group.IsBuffering()) + { + // Others are still buffering, tell this client to pause when ready + var command = NewSyncPlayCommand(SendCommandType.Pause); + var pauseAtTime = currentTime.AddMilliseconds(delay); + command.When = DateToUTCString(pauseAtTime); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + else + { + // Let other clients resume as soon as the buffering client catches up + _group.IsPaused = false; + + if (delay > _group.GetHighestPing() * 2) + { + // Client that was buffering is recovering, notifying others to resume + _group.LastActivity = currentTime.AddMilliseconds( + delay + ); + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken); + } + else + { + // Client, that was buffering, resumed playback but did not update others in time + delay = _group.GetHighestPing() * 2; + delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + + _group.LastActivity = currentTime.AddMilliseconds( + delay + ); + + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + } + } + else + { + // Group was not waiting, make sure client has latest state + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// + /// Sanitizes the PositionTicks, considers the current playing item when available. + /// + /// The PositionTicks. + /// The sanitized PositionTicks. + private long SanitizePositionTicks(long? positionTicks) + { + var ticks = positionTicks ?? 0; + ticks = ticks >= 0 ? ticks : 0; + if (_group.PlayingItem != null) + { + var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; + ticks = ticks > runTimeTicks ? runTimeTicks : ticks; + } + return ticks; + } + + /// + /// Updates ping of a session. + /// + /// The session. + /// The update. + private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) + { + // Collected pings are used to account for network latency when unpausing playback + _group.UpdatePing(session, request.Ping ?? _group.DefaulPing); + } + + /// + public GroupInfoView GetInfo() + { + return new GroupInfoView() + { + GroupId = GetGroupId().ToString(), + PlayingItemName = _group.PlayingItem.Name, + PlayingItemId = _group.PlayingItem.Id.ToString(), + PositionTicks = _group.PositionTicks, + Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToArray() + }; + } + } +} diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs new file mode 100644 index 0000000000..d3197d97b8 --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Logging; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.SyncPlay; + +namespace Emby.Server.Implementations.SyncPlay +{ + /// + /// Class SyncPlayManager. + /// + public class SyncPlayManager : ISyncPlayManager, IDisposable + { + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The user manager. + /// + private readonly IUserManager _userManager; + + /// + /// The session manager. + /// + private readonly ISessionManager _sessionManager; + + /// + /// The library manager. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// The map between sessions and groups. + /// + private readonly Dictionary _sessionToGroupMap = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The groups. + /// + private readonly Dictionary _groups = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Lock used for accesing any group. + /// + private readonly object _groupsLock = new object(); + + private bool _disposed = false; + + public SyncPlayManager( + ILogger logger, + IUserManager userManager, + ISessionManager sessionManager, + ILibraryManager libraryManager) + { + _logger = logger; + _userManager = userManager; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + + _sessionManager.SessionEnded += OnSessionManagerSessionEnded; + _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped; + } + + /// + /// Gets all groups. + /// + /// All groups. + public IEnumerable Groups => _groups.Values; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; + _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; + + _disposed = true; + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) + { + var session = e.SessionInfo; + if (!IsSessionInGroup(session)) return; + LeaveGroup(session, CancellationToken.None); + } + + private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) + { + var session = e.Session; + if (!IsSessionInGroup(session)) return; + LeaveGroup(session, CancellationToken.None); + } + + private bool IsSessionInGroup(SessionInfo session) + { + return _sessionToGroupMap.ContainsKey(session.Id); + } + + private bool HasAccessToItem(User user, Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + + // Check ParentalRating access + var hasParentalRatingAccess = true; + if (user.Policy.MaxParentalRating.HasValue) + { + hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating; + } + + if (!user.Policy.EnableAllFolders && hasParentalRatingAccess) + { + var collections = _libraryManager.GetCollectionFolders(item).Select( + folder => folder.Id.ToString("N", CultureInfo.InvariantCulture) + ); + var intersect = collections.Intersect(user.Policy.EnabledFolders); + return intersect.Any(); + } + else + { + return hasParentalRatingAccess; + } + } + + private Guid? GetSessionGroup(SessionInfo session) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + if (group != null) + { + return group.GetGroupId(); + } + else + { + return null; + } + } + + /// + public void NewGroup(SessionInfo session, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups) + { + _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.CreateGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + if (IsSessionInGroup(session)) + { + LeaveGroup(session, cancellationToken); + } + + var group = new SyncPlayController(_sessionManager, this); + _groups[group.GetGroupId().ToString()] = group; + + group.InitGroup(session, cancellationToken); + } + } + + /// + public void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.JoinGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + ISyncPlayController group; + _groups.TryGetValue(groupId, out group); + + if (group == null) + { + _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.GroupDoesNotExist + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + if (!HasAccessToItem(user, group.GetPlayingItemId())) + { + _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId()); + + var error = new GroupUpdate() + { + GroupId = group.GetGroupId().ToString(), + Type = GroupUpdateType.LibraryAccessDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + if (IsSessionInGroup(session)) + { + if (GetSessionGroup(session).Equals(groupId)) return; + LeaveGroup(session, cancellationToken); + } + + group.SessionJoin(session, request, cancellationToken); + } + } + + /// + public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken) + { + // TODO: determine what happens to users that are in a group and get their permissions revoked + lock (_groupsLock) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + + if (group == null) + { + _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.NotInGroup + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + group.SessionLeave(session, cancellationToken); + + if (group.IsGroupEmpty()) + { + _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId()); + _groups.Remove(group.GetGroupId().ToString(), out _); + } + } + } + + /// + public List ListGroups(SessionInfo session, Guid filterItemId) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + return new List(); + } + + // Filter by item if requested + if (!filterItemId.Equals(Guid.Empty)) + { + return _groups.Values.Where( + group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId()) + ).Select( + group => group.GetInfo() + ).ToList(); + } + // Otherwise show all available groups + else + { + return _groups.Values.Where( + group => HasAccessToItem(user, group.GetPlayingItemId()) + ).Select( + group => group.GetInfo() + ).ToList(); + } + } + + /// + public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.JoinGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + + if (group == null) + { + _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id); + + var error = new GroupUpdate() + { + Type = GroupUpdateType.NotInGroup + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + group.HandleRequest(session, request, cancellationToken); + } + } + + /// + public void AddSessionToGroup(SessionInfo session, ISyncPlayController group) + { + if (IsSessionInGroup(session)) + { + throw new InvalidOperationException("Session in other group already!"); + } + _sessionToGroupMap[session.Id] = group; + } + + /// + public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group) + { + if (!IsSessionInGroup(session)) + { + throw new InvalidOperationException("Session not in any group!"); + } + + ISyncPlayController tempGroup; + _sessionToGroupMap.Remove(session.Id, out tempGroup); + + if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) + { + throw new InvalidOperationException("Session was in wrong group!"); + } + } + } +} diff --git a/Emby.Server.Implementations/Syncplay/SyncplayController.cs b/Emby.Server.Implementations/Syncplay/SyncplayController.cs deleted file mode 100644 index 8cc3d1fac3..0000000000 --- a/Emby.Server.Implementations/Syncplay/SyncplayController.cs +++ /dev/null @@ -1,548 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Syncplay; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.Syncplay; - -namespace Emby.Server.Implementations.Syncplay -{ - /// - /// Class SyncplayController. - /// - /// - /// Class is not thread-safe, external locking is required when accessing methods. - /// - public class SyncplayController : ISyncplayController, IDisposable - { - /// - /// Used to filter the sessions of a group. - /// - private enum BroadcastType - { - /// - /// All sessions will receive the message. - /// - AllGroup = 0, - /// - /// Only the specified session will receive the message. - /// - CurrentSession = 1, - /// - /// All sessions, except the current one, will receive the message. - /// - AllExceptCurrentSession = 2, - /// - /// Only sessions that are not buffering will receive the message. - /// - AllReady = 3 - } - - /// - /// The session manager. - /// - private readonly ISessionManager _sessionManager; - - /// - /// The syncplay manager. - /// - private readonly ISyncplayManager _syncplayManager; - - /// - /// The group to manage. - /// - private readonly GroupInfo _group = new GroupInfo(); - - /// - public Guid GetGroupId() => _group.GroupId; - - /// - public Guid GetPlayingItemId() => _group.PlayingItem.Id; - - /// - public bool IsGroupEmpty() => _group.IsEmpty(); - - private bool _disposed = false; - - public SyncplayController( - ISessionManager sessionManager, - ISyncplayManager syncplayManager) - { - _sessionManager = sessionManager; - _syncplayManager = syncplayManager; - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and optionally managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - _disposed = true; - } - - // TODO: use this somewhere - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// - /// Converts DateTime to UTC string. - /// - /// The date to convert. - /// The UTC string. - private string DateToUTCString(DateTime date) - { - return date.ToUniversalTime().ToString("o"); - } - - /// - /// Filters sessions of this group. - /// - /// The current session. - /// The filtering type. - /// The array of sessions matching the filter. - private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type) - { - switch (type) - { - case BroadcastType.CurrentSession: - return new SessionInfo[] { from }; - case BroadcastType.AllGroup: - return _group.Participants.Values.Select( - session => session.Session - ).ToArray(); - case BroadcastType.AllExceptCurrentSession: - return _group.Participants.Values.Select( - session => session.Session - ).Where( - session => !session.Id.Equals(from.Id) - ).ToArray(); - case BroadcastType.AllReady: - return _group.Participants.Values.Where( - session => !session.IsBuffering - ).Select( - session => session.Session - ).ToArray(); - default: - return new SessionInfo[] { }; - } - } - - /// - /// Sends a GroupUpdate message to the interested sessions. - /// - /// The current session. - /// The filtering type. - /// The message to send. - /// The cancellation token. - /// The task. - private Task SendGroupUpdate(SessionInfo from, BroadcastType type, GroupUpdate message, CancellationToken cancellationToken) - { - IEnumerable GetTasks() - { - SessionInfo[] sessions = FilterSessions(from, type); - foreach (var session in sessions) - { - yield return _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), message, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); - } - - /// - /// Sends a playback command to the interested sessions. - /// - /// The current session. - /// The filtering type. - /// The message to send. - /// The cancellation token. - /// The task. - private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken) - { - IEnumerable GetTasks() - { - SessionInfo[] sessions = FilterSessions(from, type); - foreach (var session in sessions) - { - yield return _sessionManager.SendSyncplayCommand(session.Id.ToString(), message, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); - } - - /// - /// Builds a new playback command with some default values. - /// - /// The command type. - /// The SendCommand. - private SendCommand NewSyncplayCommand(SendCommandType type) - { - return new SendCommand() - { - GroupId = _group.GroupId.ToString(), - Command = type, - PositionTicks = _group.PositionTicks, - When = DateToUTCString(_group.LastActivity), - EmittedAt = DateToUTCString(DateTime.UtcNow) - }; - } - - /// - /// Builds a new group update message. - /// - /// The update type. - /// The data to send. - /// The GroupUpdate. - private GroupUpdate NewSyncplayGroupUpdate(GroupUpdateType type, T data) - { - return new GroupUpdate() - { - GroupId = _group.GroupId.ToString(), - Type = type, - Data = data - }; - } - - /// - public void InitGroup(SessionInfo session, CancellationToken cancellationToken) - { - _group.AddSession(session); - _syncplayManager.AddSessionToGroup(session, this); - - _group.PlayingItem = session.FullNowPlayingItem; - _group.IsPaused = true; - _group.PositionTicks = session.PlayState.PositionTicks ?? 0; - _group.LastActivity = DateTime.UtcNow; - - var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - var pauseCommand = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); - } - - /// - public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) - { - if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id) - { - _group.AddSession(session); - _syncplayManager.AddSessionToGroup(session, this); - - var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - - var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - - // Client join and play, syncing will happen client side - if (!_group.IsPaused) - { - var playCommand = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken); - } - else - { - var pauseCommand = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); - } - } - else - { - var playRequest = new PlayRequest(); - playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id }; - playRequest.StartPositionTicks = _group.PositionTicks; - var update = NewSyncplayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); - SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); - } - } - - /// - public void SessionLeave(SessionInfo session, CancellationToken cancellationToken) - { - _group.RemoveSession(session); - _syncplayManager.RemoveSessionFromGroup(session, this); - - var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - - var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - } - - /// - public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - // The server's job is to mantain a consistent state to which clients refer to, - // as also to notify clients of state changes. - // The actual syncing of media playback happens client side. - // Clients are aware of the server's time and use it to sync. - switch (request.Type) - { - case PlaybackRequestType.Play: - HandlePlayRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Pause: - HandlePauseRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Seek: - HandleSeekRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Buffering: - HandleBufferingRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.BufferingDone: - HandleBufferingDoneRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.UpdatePing: - HandlePingUpdateRequest(session, request); - break; - } - } - - /// - /// Handles a play action requested by a session. - /// - /// The session. - /// The play action. - /// The cancellation token. - private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (_group.IsPaused) - { - // Pick a suitable time that accounts for latency - var delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; - - // 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 - _group.IsPaused = false; - _group.LastActivity = DateTime.UtcNow.AddMilliseconds( - delay - ); - - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a pause action requested by a session. - /// - /// The session. - /// The pause action. - /// The cancellation token. - private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (!_group.IsPaused) - { - // Pause group and compute the media playback position - _group.IsPaused = true; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - _group.LastActivity; - _group.LastActivity = currentTime; - // Seek only if playback actually started - // (a pause request may be issued during the delay added to account for latency) - _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; - - var command = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a seek action requested by a session. - /// - /// The session. - /// The seek action. - /// The cancellation token. - private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - // Sanitize PositionTicks - var ticks = SanitizePositionTicks(request.PositionTicks); - - // Pause and seek - _group.IsPaused = true; - _group.PositionTicks = ticks; - _group.LastActivity = DateTime.UtcNow; - - var command = NewSyncplayCommand(SendCommandType.Seek); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - - /// - /// Handles a buffering action requested by a session. - /// - /// The session. - /// The buffering action. - /// The cancellation token. - private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (!_group.IsPaused) - { - // Pause group and compute the media playback position - _group.IsPaused = true; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - _group.LastActivity; - _group.LastActivity = currentTime; - _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; - - _group.SetBuffering(session, true); - - // Send pause command to all non-buffering sessions - var command = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.AllReady, command, cancellationToken); - - var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncplayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a buffering-done action requested by a session. - /// - /// The session. - /// The buffering-done action. - /// The cancellation token. - private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (_group.IsPaused) - { - _group.SetBuffering(session, false); - - var requestTicks = SanitizePositionTicks(request.PositionTicks); - - var when = request.When ?? DateTime.UtcNow; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - when; - var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; - var delay = _group.PositionTicks - clientPosition.Ticks; - - if (_group.IsBuffering()) - { - // Others are still buffering, tell this client to pause when ready - var command = NewSyncplayCommand(SendCommandType.Pause); - var pauseAtTime = currentTime.AddMilliseconds(delay); - command.When = DateToUTCString(pauseAtTime); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - else - { - // Let other clients resume as soon as the buffering client catches up - _group.IsPaused = false; - - if (delay > _group.GetHighestPing() * 2) - { - // Client that was buffering is recovering, notifying others to resume - _group.LastActivity = currentTime.AddMilliseconds( - delay - ); - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken); - } - else - { - // Client, that was buffering, resumed playback but did not update others in time - delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; - - _group.LastActivity = currentTime.AddMilliseconds( - delay - ); - - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - } - } - else - { - // Group was not waiting, make sure client has latest state - var command = NewSyncplayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Sanitizes the PositionTicks, considers the current playing item when available. - /// - /// The PositionTicks. - /// The sanitized PositionTicks. - private long SanitizePositionTicks(long? positionTicks) - { - var ticks = positionTicks ?? 0; - ticks = ticks >= 0 ? ticks : 0; - if (_group.PlayingItem != null) - { - var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; - ticks = ticks > runTimeTicks ? runTimeTicks : ticks; - } - return ticks; - } - - /// - /// Updates ping of a session. - /// - /// The session. - /// The update. - private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) - { - // Collected pings are used to account for network latency when unpausing playback - _group.UpdatePing(session, request.Ping ?? _group.DefaulPing); - } - - /// - public GroupInfoView GetInfo() - { - return new GroupInfoView() - { - GroupId = GetGroupId().ToString(), - PlayingItemName = _group.PlayingItem.Name, - PlayingItemId = _group.PlayingItem.Id.ToString(), - PositionTicks = _group.PositionTicks, - Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToArray() - }; - } - } -} diff --git a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs b/Emby.Server.Implementations/Syncplay/SyncplayManager.cs deleted file mode 100644 index 7074e2225e..0000000000 --- a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs +++ /dev/null @@ -1,385 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using Microsoft.Extensions.Logging; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Syncplay; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Syncplay; - -namespace Emby.Server.Implementations.Syncplay -{ - /// - /// Class SyncplayManager. - /// - public class SyncplayManager : ISyncplayManager, IDisposable - { - /// - /// The logger. - /// - private readonly ILogger _logger; - - /// - /// The user manager. - /// - private readonly IUserManager _userManager; - - /// - /// The session manager. - /// - private readonly ISessionManager _sessionManager; - - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// The map between sessions and groups. - /// - private readonly Dictionary _sessionToGroupMap = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// The groups. - /// - private readonly Dictionary _groups = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Lock used for accesing any group. - /// - private readonly object _groupsLock = new object(); - - private bool _disposed = false; - - public SyncplayManager( - ILogger logger, - IUserManager userManager, - ISessionManager sessionManager, - ILibraryManager libraryManager) - { - _logger = logger; - _userManager = userManager; - _sessionManager = sessionManager; - _libraryManager = libraryManager; - - _sessionManager.SessionEnded += OnSessionManagerSessionEnded; - _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped; - } - - /// - /// Gets all groups. - /// - /// All groups. - public IEnumerable Groups => _groups.Values; - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and optionally managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; - _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; - - _disposed = true; - } - - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) - { - var session = e.SessionInfo; - if (!IsSessionInGroup(session)) return; - LeaveGroup(session, CancellationToken.None); - } - - private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) - { - var session = e.Session; - if (!IsSessionInGroup(session)) return; - LeaveGroup(session, CancellationToken.None); - } - - private bool IsSessionInGroup(SessionInfo session) - { - return _sessionToGroupMap.ContainsKey(session.Id); - } - - private bool HasAccessToItem(User user, Guid itemId) - { - var item = _libraryManager.GetItemById(itemId); - - // Check ParentalRating access - var hasParentalRatingAccess = true; - if (user.Policy.MaxParentalRating.HasValue) - { - hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating; - } - - if (!user.Policy.EnableAllFolders && hasParentalRatingAccess) - { - var collections = _libraryManager.GetCollectionFolders(item).Select( - folder => folder.Id.ToString("N", CultureInfo.InvariantCulture) - ); - var intersect = collections.Intersect(user.Policy.EnabledFolders); - return intersect.Any(); - } - else - { - return hasParentalRatingAccess; - } - } - - private Guid? GetSessionGroup(SessionInfo session) - { - ISyncplayController group; - _sessionToGroupMap.TryGetValue(session.Id, out group); - if (group != null) - { - return group.GetGroupId(); - } - else - { - return null; - } - } - - /// - public void NewGroup(SessionInfo session, CancellationToken cancellationToken) - { - var user = _userManager.GetUserById(session.UserId); - - if (user.Policy.SyncplayAccess != SyncplayAccess.CreateAndJoinGroups) - { - _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.CreateGroupDenied - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - lock (_groupsLock) - { - if (IsSessionInGroup(session)) - { - LeaveGroup(session, cancellationToken); - } - - var group = new SyncplayController(_sessionManager, this); - _groups[group.GetGroupId().ToString()] = group; - - group.InitGroup(session, cancellationToken); - } - } - - /// - public void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken) - { - var user = _userManager.GetUserById(session.UserId); - - if (user.Policy.SyncplayAccess == SyncplayAccess.None) - { - _logger.LogWarning("JoinGroup: {0} does not have access to Syncplay.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.JoinGroupDenied - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - lock (_groupsLock) - { - ISyncplayController group; - _groups.TryGetValue(groupId, out group); - - if (group == null) - { - _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.GroupDoesNotExist - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - if (!HasAccessToItem(user, group.GetPlayingItemId())) - { - _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId()); - - var error = new GroupUpdate() - { - GroupId = group.GetGroupId().ToString(), - Type = GroupUpdateType.LibraryAccessDenied - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - if (IsSessionInGroup(session)) - { - if (GetSessionGroup(session).Equals(groupId)) return; - LeaveGroup(session, cancellationToken); - } - - group.SessionJoin(session, request, cancellationToken); - } - } - - /// - public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken) - { - // TODO: determine what happens to users that are in a group and get their permissions revoked - lock (_groupsLock) - { - ISyncplayController group; - _sessionToGroupMap.TryGetValue(session.Id, out group); - - if (group == null) - { - _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.NotInGroup - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - group.SessionLeave(session, cancellationToken); - - if (group.IsGroupEmpty()) - { - _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId()); - _groups.Remove(group.GetGroupId().ToString(), out _); - } - } - } - - /// - public List ListGroups(SessionInfo session, Guid filterItemId) - { - var user = _userManager.GetUserById(session.UserId); - - if (user.Policy.SyncplayAccess == SyncplayAccess.None) - { - return new List(); - } - - // Filter by item if requested - if (!filterItemId.Equals(Guid.Empty)) - { - return _groups.Values.Where( - group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId()) - ).Select( - group => group.GetInfo() - ).ToList(); - } - // Otherwise show all available groups - else - { - return _groups.Values.Where( - group => HasAccessToItem(user, group.GetPlayingItemId()) - ).Select( - group => group.GetInfo() - ).ToList(); - } - } - - /// - public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - var user = _userManager.GetUserById(session.UserId); - - if (user.Policy.SyncplayAccess == SyncplayAccess.None) - { - _logger.LogWarning("HandleRequest: {0} does not have access to Syncplay.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.JoinGroupDenied - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - lock (_groupsLock) - { - ISyncplayController group; - _sessionToGroupMap.TryGetValue(session.Id, out group); - - if (group == null) - { - _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id); - - var error = new GroupUpdate() - { - Type = GroupUpdateType.NotInGroup - }; - _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); - return; - } - - group.HandleRequest(session, request, cancellationToken); - } - } - - /// - public void AddSessionToGroup(SessionInfo session, ISyncplayController group) - { - if (IsSessionInGroup(session)) - { - throw new InvalidOperationException("Session in other group already!"); - } - _sessionToGroupMap[session.Id] = group; - } - - /// - public void RemoveSessionFromGroup(SessionInfo session, ISyncplayController group) - { - if (!IsSessionInGroup(session)) - { - throw new InvalidOperationException("Session not in any group!"); - } - - ISyncplayController tempGroup; - _sessionToGroupMap.Remove(session.Id, out tempGroup); - - if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) - { - throw new InvalidOperationException("Session was in wrong group!"); - } - } - } -} diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs new file mode 100644 index 0000000000..bcdc833e40 --- /dev/null +++ b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs @@ -0,0 +1,312 @@ +using System.Threading; +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.SyncPlay +{ + [Route("/SyncPlay/{SessionId}/NewGroup", "POST", Summary = "Create a new SyncPlay group")] + [Authenticated] + public class SyncPlayNewGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")] + [Authenticated] + public class SyncPlayJoinGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// + /// Gets or sets the Group id. + /// + /// The Group id to join. + [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string GroupId { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The client's currently playing item id. + [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlayingItemId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")] + [Authenticated] + public class SyncPlayLeaveGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/ListGroups", "POST", Summary = "List SyncPlay groups")] + [Authenticated] + public class SyncPlayListGroups : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// + /// Gets or sets the filter item id. + /// + /// The filter item id. + [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string FilterItemId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/PlayRequest", "POST", Summary = "Request play in SyncPlay group")] + [Authenticated] + public class SyncPlayPlayRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")] + [Authenticated] + public class SyncPlayPauseRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")] + [Authenticated] + public class SyncPlaySeekRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] + public long PositionTicks { get; set; } + } + + [Route("/SyncPlay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")] + [Authenticated] + public class SyncPlayBufferingRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// + /// Gets or sets the date used to pin PositionTicks in time. + /// + /// The date related to PositionTicks. + [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string When { get; set; } + + [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] + public long PositionTicks { get; set; } + + /// + /// Gets or sets whether this is a buffering or a buffering-done request. + /// + /// true if buffering is complete; false otherwise. + [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool BufferingDone { get; set; } + } + + [Route("/SyncPlay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")] + [Authenticated] + public class SyncPlayUpdatePing : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] + public double Ping { get; set; } + } + + /// + /// Class SyncPlayService. + /// + public class SyncPlayService : BaseApiService + { + /// + /// The session context. + /// + private readonly ISessionContext _sessionContext; + + /// + /// The SyncPlay manager. + /// + private readonly ISyncPlayManager _syncPlayManager; + + public SyncPlayService( + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + ISessionContext sessionContext, + ISyncPlayManager syncPlayManager) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _sessionContext = sessionContext; + _syncPlayManager = syncPlayManager; + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayNewGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncPlayManager.NewGroup(currentSession, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayJoinGroup request) + { + var currentSession = GetSession(_sessionContext); + var joinRequest = new JoinGroupRequest() + { + GroupId = Guid.Parse(request.GroupId) + }; + + // Both null and empty strings mean that client isn't playing anything + if (!String.IsNullOrEmpty(request.PlayingItemId)) + { + try + { + joinRequest.PlayingItemId = Guid.Parse(request.PlayingItemId); + } + catch (ArgumentNullException) + { + // Should never happen, but just in case + Logger.LogError("JoinGroup: null value for PlayingItemId. Ignoring request."); + return; + } + catch (FormatException) + { + Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId); + return; + } + } + _syncPlayManager.JoinGroup(currentSession, request.GroupId, joinRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayLeaveGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + /// The requested list of groups. + public List Post(SyncPlayListGroups request) + { + var currentSession = GetSession(_sessionContext); + var filterItemId = Guid.Empty; + if (!String.IsNullOrEmpty(request.FilterItemId)) + { + try + { + filterItemId = Guid.Parse(request.FilterItemId); + } + catch (ArgumentNullException) + { + Logger.LogWarning("ListGroups: null value for FilterItemId. Ignoring filter."); + } + catch (FormatException) + { + Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId); + } + } + return _syncPlayManager.ListGroups(currentSession, filterItemId); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayPlayRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Play + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayPauseRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Pause + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlaySeekRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Seek, + PositionTicks = request.PositionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayBufferingRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, + When = DateTime.Parse(request.When), + PositionTicks = request.PositionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// + /// Handles the specified request. + /// + /// The request. + public void Post(SyncPlayUpdatePing request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.UpdatePing, + Ping = Convert.ToInt64(request.Ping) + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + } +} diff --git a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs new file mode 100644 index 0000000000..4a9307e62f --- /dev/null +++ b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs @@ -0,0 +1,52 @@ +using System; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.SyncPlay +{ + [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")] + public class GetUtcTime : IReturnVoid + { + // Nothing + } + + /// + /// Class TimeSyncService. + /// + public class TimeSyncService : BaseApiService + { + public TimeSyncService( + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory) + : base(logger, serverConfigurationManager, httpResultFactory) + { + // Do nothing + } + + /// + /// Handles the specified request. + /// + /// The request. + /// The current UTC time response. + public UtcTimeResponse Get(GetUtcTime request) + { + // Important to keep the following line at the beginning + var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); + + var response = new UtcTimeResponse(); + response.RequestReceptionTime = requestReceptionTime; + + // Important to keep the following two lines at the end + var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); + response.ResponseTransmissionTime = responseTransmissionTime; + + // Implementing NTP on such a high level results in this useless + // information being sent. On the other hand it enables future additions. + return response; + } + } +} diff --git a/MediaBrowser.Api/Syncplay/SyncplayService.cs b/MediaBrowser.Api/Syncplay/SyncplayService.cs deleted file mode 100644 index 4b6e16762a..0000000000 --- a/MediaBrowser.Api/Syncplay/SyncplayService.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System.Threading; -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Syncplay; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Syncplay; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Syncplay -{ - [Route("/Syncplay/{SessionId}/NewGroup", "POST", Summary = "Create a new Syncplay group")] - [Authenticated] - public class SyncplayNewGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/Syncplay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing Syncplay group")] - [Authenticated] - public class SyncplayJoinGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the Group id. - /// - /// The Group id to join. - [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string GroupId { get; set; } - - /// - /// Gets or sets the playing item id. - /// - /// The client's currently playing item id. - [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string PlayingItemId { get; set; } - } - - [Route("/Syncplay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined Syncplay group")] - [Authenticated] - public class SyncplayLeaveGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/Syncplay/{SessionId}/ListGroups", "POST", Summary = "List Syncplay groups")] - [Authenticated] - public class SyncplayListGroups : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the filter item id. - /// - /// The filter item id. - [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string FilterItemId { get; set; } - } - - [Route("/Syncplay/{SessionId}/PlayRequest", "POST", Summary = "Request play in Syncplay group")] - [Authenticated] - public class SyncplayPlayRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/Syncplay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in Syncplay group")] - [Authenticated] - public class SyncplayPauseRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/Syncplay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in Syncplay group")] - [Authenticated] - public class SyncplaySeekRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] - public long PositionTicks { get; set; } - } - - [Route("/Syncplay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in Syncplay group while buffering")] - [Authenticated] - public class SyncplayBufferingRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the date used to pin PositionTicks in time. - /// - /// The date related to PositionTicks. - [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string When { get; set; } - - [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] - public long PositionTicks { get; set; } - - /// - /// Gets or sets whether this is a buffering or a buffering-done request. - /// - /// true if buffering is complete; false otherwise. - [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool BufferingDone { get; set; } - } - - [Route("/Syncplay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")] - [Authenticated] - public class SyncplayUpdatePing : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] - public double Ping { get; set; } - } - - /// - /// Class SyncplayService. - /// - public class SyncplayService : BaseApiService - { - /// - /// The session context. - /// - private readonly ISessionContext _sessionContext; - - /// - /// The Syncplay manager. - /// - private readonly ISyncplayManager _syncplayManager; - - public SyncplayService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ISessionContext sessionContext, - ISyncplayManager syncplayManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _sessionContext = sessionContext; - _syncplayManager = syncplayManager; - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayNewGroup request) - { - var currentSession = GetSession(_sessionContext); - _syncplayManager.NewGroup(currentSession, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayJoinGroup request) - { - var currentSession = GetSession(_sessionContext); - var joinRequest = new JoinGroupRequest() - { - GroupId = Guid.Parse(request.GroupId) - }; - - // Both null and empty strings mean that client isn't playing anything - if (!String.IsNullOrEmpty(request.PlayingItemId)) - { - try - { - joinRequest.PlayingItemId = Guid.Parse(request.PlayingItemId); - } - catch (ArgumentNullException) - { - // Should never happen, but just in case - Logger.LogError("JoinGroup: null value for PlayingItemId. Ignoring request."); - return; - } - catch (FormatException) - { - Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId); - return; - } - } - _syncplayManager.JoinGroup(currentSession, request.GroupId, joinRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayLeaveGroup request) - { - var currentSession = GetSession(_sessionContext); - _syncplayManager.LeaveGroup(currentSession, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - /// The requested list of groups. - public List Post(SyncplayListGroups request) - { - var currentSession = GetSession(_sessionContext); - var filterItemId = Guid.Empty; - if (!String.IsNullOrEmpty(request.FilterItemId)) - { - try - { - filterItemId = Guid.Parse(request.FilterItemId); - } - catch (ArgumentNullException) - { - Logger.LogWarning("ListGroups: null value for FilterItemId. Ignoring filter."); - } - catch (FormatException) - { - Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId); - } - } - return _syncplayManager.ListGroups(currentSession, filterItemId); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayPlayRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Play - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayPauseRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Pause - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplaySeekRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Seek, - PositionTicks = request.PositionTicks - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayBufferingRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, - When = DateTime.Parse(request.When), - PositionTicks = request.PositionTicks - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncplayUpdatePing request) - { - var currentSession = GetSession(_sessionContext); - var syncplayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.UpdatePing, - Ping = Convert.ToInt64(request.Ping) - }; - _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None); - } - } -} diff --git a/MediaBrowser.Api/Syncplay/TimeSyncService.cs b/MediaBrowser.Api/Syncplay/TimeSyncService.cs deleted file mode 100644 index 9a26ffd996..0000000000 --- a/MediaBrowser.Api/Syncplay/TimeSyncService.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.Syncplay; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Syncplay -{ - [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")] - public class GetUtcTime : IReturnVoid - { - // Nothing - } - - /// - /// Class TimeSyncService. - /// - public class TimeSyncService : BaseApiService - { - public TimeSyncService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory) - : base(logger, serverConfigurationManager, httpResultFactory) - { - // Do nothing - } - - /// - /// Handles the specified request. - /// - /// The request. - /// The current UTC time response. - public UtcTimeResponse Get(GetUtcTime request) - { - // Important to keep the following line at the beginning - var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); - - var response = new UtcTimeResponse(); - response.RequestReceptionTime = requestReceptionTime; - - // Important to keep the following two lines at the end - var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); - response.ResponseTransmissionTime = responseTransmissionTime; - - // Implementing NTP on such a high level results in this useless - // information being sent. On the other hand it enables future additions. - return response; - } - } -} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 39c065b895..4c2f834cb3 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,7 +9,7 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; using MediaBrowser.Model.Session; -using MediaBrowser.Model.Syncplay; +using MediaBrowser.Model.SyncPlay; namespace MediaBrowser.Controller.Session { @@ -142,22 +142,22 @@ namespace MediaBrowser.Controller.Session Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken); /// - /// Sends the SyncplayCommand. + /// Sends the SyncPlayCommand. /// /// The session id. /// The command. /// The cancellation token. /// Task. - Task SendSyncplayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken); + Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken); /// - /// Sends the SyncplayGroupUpdate. + /// Sends the SyncPlayGroupUpdate. /// /// The session id. /// The group update. /// The cancellation token. /// Task. - Task SendSyncplayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken); + Task SendSyncPlayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken); /// /// Sends the browse command. diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs new file mode 100644 index 0000000000..087748de08 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class GroupInfo. + /// + /// + /// Class is not thread-safe, external locking is required when accessing methods. + /// + public class GroupInfo + { + /// + /// Default ping value used for sessions. + /// + public readonly long DefaulPing = 500; + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public readonly Guid GroupId = Guid.NewGuid(); + + /// + /// Gets or sets the playing item. + /// + /// The playing item. + public BaseItem PlayingItem { get; set; } + + /// + /// Gets or sets whether playback is paused. + /// + /// Playback is paused. + public bool IsPaused { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the last activity. + /// + /// The last activity. + public DateTime LastActivity { get; set; } + + /// + /// Gets the participants. + /// + /// The participants, or members of the group. + public readonly Dictionary Participants = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Checks if a session is in this group. + /// + /// true if the session is in this group; false otherwise. + public bool ContainsSession(string sessionId) + { + return Participants.ContainsKey(sessionId); + } + + /// + /// Adds the session to the group. + /// + /// The session. + 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; + } + + /// + /// Removes the session from the group. + /// + /// The session. + + public void RemoveSession(SessionInfo session) + { + if (!ContainsSession(session.Id.ToString())) return; + GroupMember member; + Participants.Remove(session.Id.ToString(), out member); + } + + /// + /// Updates the ping of a session. + /// + /// The session. + /// The ping. + public void UpdatePing(SessionInfo session, long ping) + { + if (!ContainsSession(session.Id.ToString())) return; + Participants[session.Id.ToString()].Ping = ping; + } + + /// + /// Gets the highest ping in the group. + /// + /// The highest ping in the group. + public long GetHighestPing() + { + long max = Int64.MinValue; + foreach (var session in Participants.Values) + { + max = Math.Max(max, session.Ping); + } + return max; + } + + /// + /// Sets the session's buffering state. + /// + /// The session. + /// The state. + public void SetBuffering(SessionInfo session, bool isBuffering) + { + if (!ContainsSession(session.Id.ToString())) return; + Participants[session.Id.ToString()].IsBuffering = isBuffering; + } + + /// + /// Gets the group buffering state. + /// + /// true if there is a session buffering in the group; false otherwise. + public bool IsBuffering() + { + foreach (var session in Participants.Values) + { + if (session.IsBuffering) return true; + } + return false; + } + + /// + /// Checks if the group is empty. + /// + /// true if the group is empty; false otherwise. + 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 0000000000..a3975c334c --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class GroupMember. + /// + public class GroupMember + { + /// + /// Gets or sets whether this member is buffering. + /// + /// true if member is buffering; false otherwise. + public bool IsBuffering { get; set; } + + /// + /// Gets or sets the session. + /// + /// The session. + public SessionInfo Session { get; set; } + + /// + /// Gets or sets the ping. + /// + /// The ping. + public long Ping { get; set; } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs new file mode 100644 index 0000000000..de1fcd2591 --- /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 +{ + /// + /// Interface ISyncPlayController. + /// + public interface ISyncPlayController + { + /// + /// Gets the group id. + /// + /// The group id. + Guid GetGroupId(); + + /// + /// Gets the playing item id. + /// + /// The playing item id. + Guid GetPlayingItemId(); + + /// + /// Checks if the group is empty. + /// + /// If the group is empty. + bool IsGroupEmpty(); + + /// + /// Initializes the group with the session's info. + /// + /// The session. + /// The cancellation token. + void InitGroup(SessionInfo session, CancellationToken cancellationToken); + + /// + /// Adds the session to the group. + /// + /// The session. + /// The request. + /// The cancellation token. + void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken); + + /// + /// Removes the session from the group. + /// + /// The session. + /// The cancellation token. + void SessionLeave(SessionInfo session, CancellationToken cancellationToken); + + /// + /// Handles the requested action by the session. + /// + /// The session. + /// The requested action. + /// The cancellation token. + void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + + /// + /// Gets the info about the group for the clients. + /// + /// The group info for the clients. + 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 0000000000..6c962ec854 --- /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 +{ + /// + /// Interface ISyncPlayManager. + /// + public interface ISyncPlayManager + { + /// + /// Creates a new group. + /// + /// The session that's creating the group. + /// The cancellation token. + void NewGroup(SessionInfo session, CancellationToken cancellationToken); + + /// + /// Adds the session to a group. + /// + /// The session. + /// The group id. + /// The request. + /// The cancellation token. + void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken); + + /// + /// Removes the session from a group. + /// + /// The session. + /// The cancellation token. + void LeaveGroup(SessionInfo session, CancellationToken cancellationToken); + + /// + /// Gets list of available groups for a session. + /// + /// The session. + /// The item id to filter by. + /// The list of available groups. + List ListGroups(SessionInfo session, Guid filterItemId); + + /// + /// Handle a request by a session in a group. + /// + /// The session. + /// The request. + /// The cancellation token. + void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + + /// + /// Maps a session to a group. + /// + /// The session. + /// The group. + /// + void AddSessionToGroup(SessionInfo session, ISyncPlayController group); + + /// + /// Unmaps a session from a group. + /// + /// The session. + /// The group. + /// + void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group); + } +} diff --git a/MediaBrowser.Controller/Syncplay/GroupInfo.cs b/MediaBrowser.Controller/Syncplay/GroupInfo.cs deleted file mode 100644 index c01fead836..0000000000 --- a/MediaBrowser.Controller/Syncplay/GroupInfo.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Session; - -namespace MediaBrowser.Controller.Syncplay -{ - /// - /// Class GroupInfo. - /// - /// - /// Class is not thread-safe, external locking is required when accessing methods. - /// - public class GroupInfo - { - /// - /// Default ping value used for sessions. - /// - public readonly long DefaulPing = 500; - /// - /// Gets or sets the group identifier. - /// - /// The group identifier. - public readonly Guid GroupId = Guid.NewGuid(); - - /// - /// Gets or sets the playing item. - /// - /// The playing item. - public BaseItem PlayingItem { get; set; } - - /// - /// Gets or sets whether playback is paused. - /// - /// Playback is paused. - public bool IsPaused { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long PositionTicks { get; set; } - - /// - /// Gets or sets the last activity. - /// - /// The last activity. - public DateTime LastActivity { get; set; } - - /// - /// Gets the participants. - /// - /// The participants, or members of the group. - public readonly Dictionary Participants = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Checks if a session is in this group. - /// - /// true if the session is in this group; false otherwise. - public bool ContainsSession(string sessionId) - { - return Participants.ContainsKey(sessionId); - } - - /// - /// Adds the session to the group. - /// - /// The session. - 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; - } - - /// - /// Removes the session from the group. - /// - /// The session. - - public void RemoveSession(SessionInfo session) - { - if (!ContainsSession(session.Id.ToString())) return; - GroupMember member; - Participants.Remove(session.Id.ToString(), out member); - } - - /// - /// Updates the ping of a session. - /// - /// The session. - /// The ping. - public void UpdatePing(SessionInfo session, long ping) - { - if (!ContainsSession(session.Id.ToString())) return; - Participants[session.Id.ToString()].Ping = ping; - } - - /// - /// Gets the highest ping in the group. - /// - /// The highest ping in the group. - public long GetHighestPing() - { - long max = Int64.MinValue; - foreach (var session in Participants.Values) - { - max = Math.Max(max, session.Ping); - } - return max; - } - - /// - /// Sets the session's buffering state. - /// - /// The session. - /// The state. - public void SetBuffering(SessionInfo session, bool isBuffering) - { - if (!ContainsSession(session.Id.ToString())) return; - Participants[session.Id.ToString()].IsBuffering = isBuffering; - } - - /// - /// Gets the group buffering state. - /// - /// true if there is a session buffering in the group; false otherwise. - public bool IsBuffering() - { - foreach (var session in Participants.Values) - { - if (session.IsBuffering) return true; - } - return false; - } - - /// - /// Checks if the group is empty. - /// - /// true if the group is empty; false otherwise. - public bool IsEmpty() - { - return Participants.Count == 0; - } - } -} diff --git a/MediaBrowser.Controller/Syncplay/GroupMember.cs b/MediaBrowser.Controller/Syncplay/GroupMember.cs deleted file mode 100644 index 7630428d76..0000000000 --- a/MediaBrowser.Controller/Syncplay/GroupMember.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MediaBrowser.Controller.Session; - -namespace MediaBrowser.Controller.Syncplay -{ - /// - /// Class GroupMember. - /// - public class GroupMember - { - /// - /// Gets or sets whether this member is buffering. - /// - /// true if member is buffering; false otherwise. - public bool IsBuffering { get; set; } - - /// - /// Gets or sets the session. - /// - /// The session. - public SessionInfo Session { get; set; } - - /// - /// Gets or sets the ping. - /// - /// The ping. - public long Ping { get; set; } - } -} diff --git a/MediaBrowser.Controller/Syncplay/ISyncplayController.cs b/MediaBrowser.Controller/Syncplay/ISyncplayController.cs deleted file mode 100644 index 34eae40624..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 -{ - /// - /// Interface ISyncplayController. - /// - public interface ISyncplayController - { - /// - /// Gets the group id. - /// - /// The group id. - Guid GetGroupId(); - - /// - /// Gets the playing item id. - /// - /// The playing item id. - Guid GetPlayingItemId(); - - /// - /// Checks if the group is empty. - /// - /// If the group is empty. - bool IsGroupEmpty(); - - /// - /// Initializes the group with the session's info. - /// - /// The session. - /// The cancellation token. - void InitGroup(SessionInfo session, CancellationToken cancellationToken); - - /// - /// Adds the session to the group. - /// - /// The session. - /// The request. - /// The cancellation token. - void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken); - - /// - /// Removes the session from the group. - /// - /// The session. - /// The cancellation token. - void SessionLeave(SessionInfo session, CancellationToken cancellationToken); - - /// - /// Handles the requested action by the session. - /// - /// The session. - /// The requested action. - /// The cancellation token. - void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); - - /// - /// Gets the info about the group for the clients. - /// - /// The group info for the clients. - GroupInfoView GetInfo(); - } -} \ No newline at end of file diff --git a/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs b/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs deleted file mode 100644 index fbc208d27d..0000000000 --- a/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Syncplay; - -namespace MediaBrowser.Controller.Syncplay -{ - /// - /// Interface ISyncplayManager. - /// - public interface ISyncplayManager - { - /// - /// Creates a new group. - /// - /// The session that's creating the group. - /// The cancellation token. - void NewGroup(SessionInfo session, CancellationToken cancellationToken); - - /// - /// Adds the session to a group. - /// - /// The session. - /// The group id. - /// The request. - /// The cancellation token. - void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken); - - /// - /// Removes the session from a group. - /// - /// The session. - /// The cancellation token. - void LeaveGroup(SessionInfo session, CancellationToken cancellationToken); - - /// - /// Gets list of available groups for a session. - /// - /// The session. - /// The item id to filter by. - /// The list of available groups. - List ListGroups(SessionInfo session, Guid filterItemId); - - /// - /// Handle a request by a session in a group. - /// - /// The session. - /// The request. - /// The cancellation token. - void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); - - /// - /// Maps a session to a group. - /// - /// The session. - /// The group. - /// - void AddSessionToGroup(SessionInfo session, ISyncplayController group); - - /// - /// Unmaps a session from a group. - /// - /// The session. - /// The group. - /// - void RemoveSessionFromGroup(SessionInfo session, ISyncplayController group); - } -} diff --git a/MediaBrowser.Model/Configuration/SyncplayAccess.cs b/MediaBrowser.Model/Configuration/SyncplayAccess.cs index cddf68c42d..d891a8167a 100644 --- a/MediaBrowser.Model/Configuration/SyncplayAccess.cs +++ b/MediaBrowser.Model/Configuration/SyncplayAccess.cs @@ -1,9 +1,9 @@ namespace MediaBrowser.Model.Configuration { /// - /// Enum SyncplayAccess. + /// Enum SyncPlayAccess. /// - public enum SyncplayAccess + public enum SyncPlayAccess { /// /// User can create groups and join them. @@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Configuration JoinGroups, /// - /// Syncplay is disabled for the user. + /// SyncPlay is disabled for the user. /// None } diff --git a/MediaBrowser.Model/SyncPlay/GroupInfoView.cs b/MediaBrowser.Model/SyncPlay/GroupInfoView.cs new file mode 100644 index 0000000000..7b833506ba --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupInfoView.cs @@ -0,0 +1,38 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class GroupInfoView. + /// + public class GroupInfoView + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The playing item id. + public string PlayingItemId { get; set; } + + /// + /// Gets or sets the playing item name. + /// + /// The playing item name. + public string PlayingItemName { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the participants. + /// + /// The participants. + public string[] Participants { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdate.cs b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs new file mode 100644 index 0000000000..895702f3dd --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs @@ -0,0 +1,26 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class GroupUpdate. + /// + public class GroupUpdate + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the update type. + /// + /// The update type. + public GroupUpdateType Type { get; set; } + + /// + /// Gets or sets the data. + /// + /// The data. + public T Data { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs new file mode 100644 index 0000000000..89d2457872 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs @@ -0,0 +1,53 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Enum GroupUpdateType. + /// + public enum GroupUpdateType + { + /// + /// The user-joined update. Tells members of a group about a new user. + /// + UserJoined, + /// + /// The user-left update. Tells members of a group that a user left. + /// + UserLeft, + /// + /// The group-joined update. Tells a user that the group has been joined. + /// + GroupJoined, + /// + /// The group-left update. Tells a user that the group has been left. + /// + GroupLeft, + /// + /// The group-wait update. Tells members of the group that a user is buffering. + /// + GroupWait, + /// + /// The prepare-session update. Tells a user to load some content. + /// + PrepareSession, + /// + /// The not-in-group error. Tells a user that they don't belong to a group. + /// + NotInGroup, + /// + /// The group-does-not-exist error. Sent when trying to join a non-existing group. + /// + GroupDoesNotExist, + /// + /// The create-group-denied error. Sent when a user tries to create a group without required permissions. + /// + CreateGroupDenied, + /// + /// The join-group-denied error. Sent when a user tries to join a group without required permissions. + /// + JoinGroupDenied, + /// + /// The library-access-denied error. Sent when a user tries to join a group without required access to the library. + /// + LibraryAccessDenied + } +} diff --git a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs new file mode 100644 index 0000000000..d67b6bd555 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs @@ -0,0 +1,22 @@ +using System; + +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class JoinGroupRequest. + /// + public class JoinGroupRequest + { + /// + /// Gets or sets the Group id. + /// + /// The Group id to join. + public Guid GroupId { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The client's currently playing item id. + public Guid PlayingItemId { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs new file mode 100644 index 0000000000..9de23194e3 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs @@ -0,0 +1,34 @@ +using System; + +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class PlaybackRequest. + /// + public class PlaybackRequest + { + /// + /// Gets or sets the request type. + /// + /// The request type. + public PlaybackRequestType Type { get; set; } + + /// + /// Gets or sets when the request has been made by the client. + /// + /// The date of the request. + public DateTime? When { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long? PositionTicks { get; set; } + + /// + /// Gets or sets the ping time. + /// + /// The ping time. + public long? Ping { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs new file mode 100644 index 0000000000..f1e175fdec --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs @@ -0,0 +1,33 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Enum PlaybackRequestType + /// + public enum PlaybackRequestType + { + /// + /// A user is requesting a play command for the group. + /// + Play = 0, + /// + /// A user is requesting a pause command for the group. + /// + Pause = 1, + /// + /// A user is requesting a seek command for the group. + /// + Seek = 2, + /// + /// A user is signaling that playback is buffering. + /// + Buffering = 3, + /// + /// A user is signaling that playback resumed. + /// + BufferingDone = 4, + /// + /// A user is reporting its ping. + /// + UpdatePing = 5 + } +} diff --git a/MediaBrowser.Model/SyncPlay/SendCommand.cs b/MediaBrowser.Model/SyncPlay/SendCommand.cs new file mode 100644 index 0000000000..0f06e381f1 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SendCommand.cs @@ -0,0 +1,38 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class SendCommand. + /// + public class SendCommand + { + /// + /// Gets or sets the group identifier. + /// + /// The group identifier. + public string GroupId { get; set; } + + /// + /// Gets or sets the UTC time when to execute the command. + /// + /// The UTC time when to execute the command. + public string When { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long? PositionTicks { get; set; } + + /// + /// Gets or sets the command. + /// + /// The command. + public SendCommandType Command { get; set; } + + /// + /// Gets or sets the UTC time when this command has been emitted. + /// + /// The UTC time when this command has been emitted. + public string EmittedAt { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/SendCommandType.cs b/MediaBrowser.Model/SyncPlay/SendCommandType.cs new file mode 100644 index 0000000000..1137198715 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SendCommandType.cs @@ -0,0 +1,21 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Enum SendCommandType. + /// + public enum SendCommandType + { + /// + /// The play command. Instructs users to start playback. + /// + Play = 0, + /// + /// The pause command. Instructs users to pause playback. + /// + Pause = 1, + /// + /// The seek command. Instructs users to seek to a specified time. + /// + Seek = 2 + } +} diff --git a/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs b/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs new file mode 100644 index 0000000000..0a60361544 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs @@ -0,0 +1,20 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Class UtcTimeResponse. + /// + public class UtcTimeResponse + { + /// + /// Gets or sets the UTC time when request has been received. + /// + /// The UTC time when request has been received. + public string RequestReceptionTime { get; set; } + + /// + /// Gets or sets the UTC time when response has been sent. + /// + /// The UTC time when response has been sent. + public string ResponseTransmissionTime { get; set; } + } +} diff --git a/MediaBrowser.Model/Syncplay/GroupInfoView.cs b/MediaBrowser.Model/Syncplay/GroupInfoView.cs deleted file mode 100644 index 50ad70630f..0000000000 --- a/MediaBrowser.Model/Syncplay/GroupInfoView.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class GroupInfoView. - /// - public class GroupInfoView - { - /// - /// Gets or sets the group identifier. - /// - /// The group identifier. - public string GroupId { get; set; } - - /// - /// Gets or sets the playing item id. - /// - /// The playing item id. - public string PlayingItemId { get; set; } - - /// - /// Gets or sets the playing item name. - /// - /// The playing item name. - public string PlayingItemName { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long PositionTicks { get; set; } - - /// - /// Gets or sets the participants. - /// - /// The participants. - public string[] Participants { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/GroupUpdate.cs b/MediaBrowser.Model/Syncplay/GroupUpdate.cs deleted file mode 100644 index cc49e92a9c..0000000000 --- a/MediaBrowser.Model/Syncplay/GroupUpdate.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class GroupUpdate. - /// - public class GroupUpdate - { - /// - /// Gets or sets the group identifier. - /// - /// The group identifier. - public string GroupId { get; set; } - - /// - /// Gets or sets the update type. - /// - /// The update type. - public GroupUpdateType Type { get; set; } - - /// - /// Gets or sets the data. - /// - /// The data. - public T Data { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/GroupUpdateType.cs b/MediaBrowser.Model/Syncplay/GroupUpdateType.cs deleted file mode 100644 index 9f40f9577f..0000000000 --- a/MediaBrowser.Model/Syncplay/GroupUpdateType.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Enum GroupUpdateType. - /// - public enum GroupUpdateType - { - /// - /// The user-joined update. Tells members of a group about a new user. - /// - UserJoined, - /// - /// The user-left update. Tells members of a group that a user left. - /// - UserLeft, - /// - /// The group-joined update. Tells a user that the group has been joined. - /// - GroupJoined, - /// - /// The group-left update. Tells a user that the group has been left. - /// - GroupLeft, - /// - /// The group-wait update. Tells members of the group that a user is buffering. - /// - GroupWait, - /// - /// The prepare-session update. Tells a user to load some content. - /// - PrepareSession, - /// - /// The not-in-group error. Tells a user that they don't belong to a group. - /// - NotInGroup, - /// - /// The group-does-not-exist error. Sent when trying to join a non-existing group. - /// - GroupDoesNotExist, - /// - /// The create-group-denied error. Sent when a user tries to create a group without required permissions. - /// - CreateGroupDenied, - /// - /// The join-group-denied error. Sent when a user tries to join a group without required permissions. - /// - JoinGroupDenied, - /// - /// The library-access-denied error. Sent when a user tries to join a group without required access to the library. - /// - LibraryAccessDenied - } -} diff --git a/MediaBrowser.Model/Syncplay/JoinGroupRequest.cs b/MediaBrowser.Model/Syncplay/JoinGroupRequest.cs deleted file mode 100644 index 8d8a2646ac..0000000000 --- a/MediaBrowser.Model/Syncplay/JoinGroupRequest.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class JoinGroupRequest. - /// - public class JoinGroupRequest - { - /// - /// Gets or sets the Group id. - /// - /// The Group id to join. - public Guid GroupId { get; set; } - - /// - /// Gets or sets the playing item id. - /// - /// The client's currently playing item id. - public Guid PlayingItemId { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/PlaybackRequest.cs b/MediaBrowser.Model/Syncplay/PlaybackRequest.cs deleted file mode 100644 index ba97641f63..0000000000 --- a/MediaBrowser.Model/Syncplay/PlaybackRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class PlaybackRequest. - /// - public class PlaybackRequest - { - /// - /// Gets or sets the request type. - /// - /// The request type. - public PlaybackRequestType Type { get; set; } - - /// - /// Gets or sets when the request has been made by the client. - /// - /// The date of the request. - public DateTime? When { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long? PositionTicks { get; set; } - - /// - /// Gets or sets the ping time. - /// - /// The ping time. - public long? Ping { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs b/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs deleted file mode 100644 index b3d49d09ef..0000000000 --- a/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Enum PlaybackRequestType - /// - public enum PlaybackRequestType - { - /// - /// A user is requesting a play command for the group. - /// - Play = 0, - /// - /// A user is requesting a pause command for the group. - /// - Pause = 1, - /// - /// A user is requesting a seek command for the group. - /// - Seek = 2, - /// - /// A user is signaling that playback is buffering. - /// - Buffering = 3, - /// - /// A user is signaling that playback resumed. - /// - BufferingDone = 4, - /// - /// A user is reporting its ping. - /// - UpdatePing = 5 - } -} diff --git a/MediaBrowser.Model/Syncplay/SendCommand.cs b/MediaBrowser.Model/Syncplay/SendCommand.cs deleted file mode 100644 index d9f3914030..0000000000 --- a/MediaBrowser.Model/Syncplay/SendCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class SendCommand. - /// - public class SendCommand - { - /// - /// Gets or sets the group identifier. - /// - /// The group identifier. - public string GroupId { get; set; } - - /// - /// Gets or sets the UTC time when to execute the command. - /// - /// The UTC time when to execute the command. - public string When { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long? PositionTicks { get; set; } - - /// - /// Gets or sets the command. - /// - /// The command. - public SendCommandType Command { get; set; } - - /// - /// Gets or sets the UTC time when this command has been emitted. - /// - /// The UTC time when this command has been emitted. - public string EmittedAt { get; set; } - } -} diff --git a/MediaBrowser.Model/Syncplay/SendCommandType.cs b/MediaBrowser.Model/Syncplay/SendCommandType.cs deleted file mode 100644 index 02e4774d0d..0000000000 --- a/MediaBrowser.Model/Syncplay/SendCommandType.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Enum SendCommandType. - /// - public enum SendCommandType - { - /// - /// The play command. Instructs users to start playback. - /// - Play = 0, - /// - /// The pause command. Instructs users to pause playback. - /// - Pause = 1, - /// - /// The seek command. Instructs users to seek to a specified time. - /// - Seek = 2 - } -} diff --git a/MediaBrowser.Model/Syncplay/UtcTimeResponse.cs b/MediaBrowser.Model/Syncplay/UtcTimeResponse.cs deleted file mode 100644 index f7887dc332..0000000000 --- a/MediaBrowser.Model/Syncplay/UtcTimeResponse.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MediaBrowser.Model.Syncplay -{ - /// - /// Class UtcTimeResponse. - /// - public class UtcTimeResponse - { - /// - /// Gets or sets the UTC time when request has been received. - /// - /// The UTC time when request has been received. - public string RequestReceptionTime { get; set; } - - /// - /// Gets or sets the UTC time when response has been sent. - /// - /// The UTC time when response has been sent. - public string ResponseTransmissionTime { get; set; } - } -} diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index cf576c3582..3e027e8312 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -81,10 +81,10 @@ namespace MediaBrowser.Model.Users public string PasswordResetProviderId { get; set; } /// - /// Gets or sets a value indicating what Syncplay features the user can access. + /// Gets or sets a value indicating what SyncPlay features the user can access. /// - /// Access level to Syncplay features. - public SyncplayAccess SyncplayAccess { get; set; } + /// Access level to SyncPlay features. + public SyncPlayAccess SyncPlayAccess { get; set; } public UserPolicy() { @@ -131,7 +131,7 @@ namespace MediaBrowser.Model.Users EnableContentDownloading = true; EnablePublicSharing = true; EnableRemoteAccess = true; - SyncplayAccess = SyncplayAccess.CreateAndJoinGroups; + SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups; } } } -- cgit v1.2.3 From b94afc597c4d51f67552c9ba2c25bdb8df6d8599 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 14 May 2020 17:13:45 -0400 Subject: Address review comments --- Emby.Server.Implementations/ApplicationHost.cs | 11 --- .../Configuration/ServerConfigurationManager.cs | 6 -- .../Emby.Server.Implementations.csproj | 3 +- Jellyfin.Data/Entities/ActivityLog.cs | 83 +++++++++++----------- Jellyfin.Data/Jellyfin.Data.csproj | 6 +- .../Activity/ActivityManager.cs | 14 ++-- .../Jellyfin.Server.Implementations.csproj | 8 +++ Jellyfin.Server.Implementations/JellyfinDb.cs | 2 +- .../JellyfinDbProvider.cs | 2 +- .../20200502231229_InitialSchema.Designer.cs | 73 ------------------- .../Migrations/20200502231229_InitialSchema.cs | 46 ------------ .../20200514181226_AddActivityLog.Designer.cs | 72 +++++++++++++++++++ .../Migrations/20200514181226_AddActivityLog.cs | 46 ++++++++++++ .../Migrations/DesignTimeJellyfinDbFactory.cs | 7 +- .../Migrations/JellyfinDbModelSnapshot.cs | 4 +- Jellyfin.Server/CoreAppHost.cs | 14 ++++ Jellyfin.Server/Jellyfin.Server.csproj | 8 +-- MediaBrowser.Api/System/ActivityLogService.cs | 13 +++- MediaBrowser.Model/Activity/IActivityManager.cs | 3 +- .../Configuration/ServerConfiguration.cs | 2 - MediaBrowser.Model/Devices/DeviceOptions.cs | 9 +++ MediaBrowser.Model/Devices/DevicesOptions.cs | 23 ------ 22 files changed, 221 insertions(+), 234 deletions(-) delete mode 100644 Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs create mode 100644 MediaBrowser.Model/Devices/DeviceOptions.cs delete mode 100644 MediaBrowser.Model/Devices/DevicesOptions.cs (limited to 'MediaBrowser.Model/Configuration') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 371b5a5b9b..8e5c3c9cf2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -46,8 +46,6 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Jellyfin.Server.Implementations; -using Jellyfin.Server.Implementations.Activity; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -547,13 +545,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - // TODO: properly set up scoping and switch to AddDbContextPool - serviceCollection.AddDbContext( - options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), - ServiceLifetime.Transient); - - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton(); @@ -664,8 +655,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index a6eaf2d0a3..305e67e8c3 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration changed = true; } - if (!config.CameraUploadUpgraded) - { - config.CameraUploadUpgraded = true; - changed = true; - } - if (!config.CollectionsUpgraded) { config.CollectionsUpgraded = true; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index dccbe2a9a6..896e4310e7 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -9,7 +9,6 @@ - @@ -51,7 +50,7 @@ - netcoreapp3.1 + netstandard2.1 false true diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs index df3026a770..8fbf6eaabf 100644 --- a/Jellyfin.Data/Entities/ActivityLog.cs +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -5,34 +5,18 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Data.Entities { - public partial class ActivityLog + /// + /// An entity referencing an activity log entry. + /// + public partial class ActivityLog : ISavingChanges { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected ActivityLog() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static ActivityLog CreateActivityLogUnsafe() - { - return new ActivityLog(); - } - /// - /// Public constructor with required data + /// Initializes a new instance of the class. + /// Public constructor with required data. /// - /// - /// - /// - /// - /// + /// The name. + /// The type. + /// The user id. public ActivityLog(string name, string type, Guid userId) { if (string.IsNullOrEmpty(name)) @@ -54,14 +38,21 @@ namespace Jellyfin.Data.Entities Init(); } + /// + /// Initializes a new instance of the class. + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected ActivityLog() + { + Init(); + } + /// /// Static create function (for use in LINQ queries, etc.) /// - /// - /// - /// - /// - /// + /// The name. + /// The type. + /// The user's id. public static ActivityLog Create(string name, string type, Guid userId) { return new ActivityLog(name, type, userId); @@ -72,7 +63,8 @@ namespace Jellyfin.Data.Entities *************************************************************************/ /// - /// Identity, Indexed, Required + /// Gets the identity of this instance. + /// This is the key in the backing database. /// [Key] [Required] @@ -80,7 +72,8 @@ namespace Jellyfin.Data.Entities public int Id { get; protected set; } /// - /// Required, Max length = 512 + /// Gets or sets the name. + /// Required, Max length = 512. /// [Required] [MaxLength(512)] @@ -88,21 +81,24 @@ namespace Jellyfin.Data.Entities public string Name { get; set; } /// - /// Max length = 512 + /// Gets or sets the overview. + /// Max length = 512. /// [MaxLength(512)] [StringLength(512)] public string Overview { get; set; } /// - /// Max length = 512 + /// Gets or sets the short overview. + /// Max length = 512. /// [MaxLength(512)] [StringLength(512)] public string ShortOverview { get; set; } /// - /// Required, Max length = 256 + /// Gets or sets the type. + /// Required, Max length = 256. /// [Required] [MaxLength(256)] @@ -110,41 +106,48 @@ namespace Jellyfin.Data.Entities public string Type { get; set; } /// - /// Required + /// Gets or sets the user id. + /// Required. /// [Required] public Guid UserId { get; set; } /// - /// Max length = 256 + /// Gets or sets the item id. + /// Max length = 256. /// [MaxLength(256)] [StringLength(256)] public string ItemId { get; set; } /// - /// Required + /// Gets or sets the date created. This should be in UTC. + /// Required. /// [Required] public DateTime DateCreated { get; set; } /// - /// Required + /// Gets or sets the log severity. Default is . + /// Required. /// [Required] public LogLevel LogSeverity { get; set; } /// + /// Gets or sets the row version. /// Required, ConcurrencyToken. /// [ConcurrencyCheck] [Required] public uint RowVersion { get; set; } + partial void Init(); + + /// public void OnSavingChanges() { RowVersion++; } } } - diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 8eae366bab..b2a3f7eb34 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -17,14 +17,10 @@ - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 531b529dce..0b398b60cd 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -50,31 +50,31 @@ namespace Jellyfin.Server.Implementations.Activity /// public QueryResult GetPagedResult( - Func, IEnumerable> func, + Func, IQueryable> func, int? startIndex, int? limit) { using var dbContext = _provider.CreateContext(); - var result = func.Invoke(dbContext.ActivityLogs).AsQueryable(); + var query = func(dbContext.ActivityLogs).OrderByDescending(entry => entry.DateCreated).AsQueryable(); if (startIndex.HasValue) { - result = result.Where(entry => entry.Id >= startIndex.Value); + query = query.Skip(startIndex.Value); } if (limit.HasValue) { - result = result.OrderByDescending(entry => entry.DateCreated).Take(limit.Value); + query = query.Take(limit.Value); } // This converts the objects from the new database model to the old for compatibility with the existing API. - var list = result.Select(entry => ConvertToOldModel(entry)).ToList(); + var list = query.AsEnumerable().Select(ConvertToOldModel).ToList(); - return new QueryResult() + return new QueryResult { Items = list, - TotalRecordCount = list.Count + TotalRecordCount = dbContext.ActivityLogs.Count() }; } diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index a31f28f64a..149ca50209 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -25,6 +25,14 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 6fc8d251b8..23714b24a1 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -110,7 +110,7 @@ namespace Jellyfin.Server.Implementations foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified)) { var saveEntity = entity.Entity as ISavingChanges; - saveEntity.OnSavingChanges(); + saveEntity?.OnSavingChanges(); } return base.SaveChanges(); diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs index 8fdeab0887..eab531d386 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations /// The newly created context. public JellyfinDb CreateContext() { - return _serviceProvider.GetService(); + return _serviceProvider.GetRequiredService(); } } } diff --git a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs deleted file mode 100644 index e1ee9b34aa..0000000000 --- a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs +++ /dev/null @@ -1,73 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -// -using System; -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Jellyfin.Server.Implementations.Migrations -{ - [DbContext(typeof(JellyfinDb))] - [Migration("20200502231229_InitialSchema")] - partial class InitialSchema - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "3.1.3"); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("Overview") - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ShortOverview") - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ActivityLog"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs b/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs deleted file mode 100644 index 42fac865ce..0000000000 --- a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs +++ /dev/null @@ -1,46 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Jellyfin.Server.Implementations.Migrations -{ - public partial class InitialSchema : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "jellyfin"); - - migrationBuilder.CreateTable( - name: "ActivityLog", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(maxLength: 512, nullable: false), - Overview = table.Column(maxLength: 512, nullable: true), - ShortOverview = table.Column(maxLength: 512, nullable: true), - Type = table.Column(maxLength: 256, nullable: false), - UserId = table.Column(nullable: false), - ItemId = table.Column(maxLength: 256, nullable: true), - DateCreated = table.Column(nullable: false), - LogSeverity = table.Column(nullable: false), - RowVersion = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ActivityLog", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ActivityLog", - schema: "jellyfin"); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs new file mode 100644 index 0000000000..98a83b7450 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs @@ -0,0 +1,72 @@ +#pragma warning disable CS1591 + +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20200514181226_AddActivityLog")] + partial class AddActivityLog + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs new file mode 100644 index 0000000000..5e0b454d8b --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs @@ -0,0 +1,46 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddActivityLog : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "jellyfin"); + + migrationBuilder.CreateTable( + name: "ActivityLogs", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 512, nullable: false), + Overview = table.Column(maxLength: 512, nullable: true), + ShortOverview = table.Column(maxLength: 512, nullable: true), + Type = table.Column(maxLength: 256, nullable: false), + UserId = table.Column(nullable: false), + ItemId = table.Column(maxLength: 256, nullable: true), + DateCreated = table.Column(nullable: false), + LogSeverity = table.Column(nullable: false), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ActivityLogs", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ActivityLogs", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs index 23a0fdc784..a1b58eb5a9 100644 --- a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs @@ -1,6 +1,3 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -15,7 +12,9 @@ namespace Jellyfin.Server.Implementations.Migrations public JellyfinDb CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite("Data Source=jellyfin.db"); + optionsBuilder.UseSqlite( + "Data Source=jellyfin.db", + opt => opt.MigrationsAssembly("Jellyfin.Migrations")); return new JellyfinDb(optionsBuilder.Options); } diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 27f5fe24b0..1e7ffd2359 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1,9 +1,7 @@ // using System; -using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { @@ -60,7 +58,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.ToTable("ActivityLog"); + b.ToTable("ActivityLogs"); }); #pragma warning restore 612, 618 } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index f678e714c1..331a32c737 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,12 +1,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; +using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Activity; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -56,6 +61,15 @@ namespace Jellyfin.Server Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); } + // TODO: Set up scoping and use AddDbContextPool + serviceCollection.AddDbContext( + options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), + ServiceLifetime.Transient); + + serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + base.RegisterServices(serviceCollection); } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 4194070aa9..9eec6ed4eb 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -13,9 +13,6 @@ true true enable - - - True @@ -44,10 +41,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -67,6 +60,7 @@ + diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs index f2c37d7117..a6bacad4fc 100644 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ b/MediaBrowser.Api/System/ActivityLogService.cs @@ -1,3 +1,7 @@ +using System; +using System.Globalization; +using System.Linq; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; @@ -47,7 +51,14 @@ namespace MediaBrowser.Api.System public object Get(GetActivityLogs request) { - var result = _activityManager.GetPagedResult(request.StartIndex, request.Limit); + DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ? + (DateTime?)null : + DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + + var filterFunc = new Func, IQueryable>( + entries => entries.Where(entry => entry.DateCreated >= minDate)); + + var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit); return ToOptimizedResult(result); } diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index 6742dc8fc4..9dab5e77b7 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; @@ -21,7 +20,7 @@ namespace MediaBrowser.Model.Activity QueryResult GetPagedResult(int? startIndex, int? limit); QueryResult GetPagedResult( - Func, IEnumerable> func, + Func, IQueryable> func, int? startIndex, int? limit); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 22a42322a6..1f5981f101 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -79,8 +79,6 @@ namespace MediaBrowser.Model.Configuration public bool EnableRemoteAccess { get; set; } - public bool CameraUploadUpgraded { get; set; } - public bool CollectionsUpgraded { get; set; } /// diff --git a/MediaBrowser.Model/Devices/DeviceOptions.cs b/MediaBrowser.Model/Devices/DeviceOptions.cs new file mode 100644 index 0000000000..8b77fd7fc3 --- /dev/null +++ b/MediaBrowser.Model/Devices/DeviceOptions.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Devices +{ + public class DeviceOptions + { + public string CustomName { get; set; } + } +} diff --git a/MediaBrowser.Model/Devices/DevicesOptions.cs b/MediaBrowser.Model/Devices/DevicesOptions.cs deleted file mode 100644 index 02570650e9..0000000000 --- a/MediaBrowser.Model/Devices/DevicesOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Devices -{ - public class DevicesOptions - { - public string[] EnabledCameraUploadDevices { get; set; } - public string CameraUploadPath { get; set; } - public bool EnableCameraUploadSubfolders { get; set; } - - public DevicesOptions() - { - EnabledCameraUploadDevices = Array.Empty(); - } - } - - public class DeviceOptions - { - public string CustomName { get; set; } - } -} -- cgit v1.2.3