Merge branch 'master' into userdb-efcore
# Conflicts: # Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs # Emby.Server.Implementations/ApplicationHost.cs # Emby.Server.Implementations/Devices/DeviceManager.cs # Jellyfin.Server/Jellyfin.Server.csproj # Jellyfin.Server/Migrations/MigrationRunner.cs # MediaBrowser.Controller/Devices/IDeviceManager.cs
This commit is contained in:
commit
aca7e221d8
|
@ -914,7 +914,8 @@ namespace Emby.Dlna.PlayTo
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
|
/// <inheritdoc />
|
||||||
|
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
|
@ -930,10 +931,12 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
||||||
|
|
|
@ -227,7 +227,7 @@ namespace Emby.Naming.Video
|
||||||
}
|
}
|
||||||
|
|
||||||
return remainingFiles
|
return remainingFiles
|
||||||
.Where(i => i.ExtraType == null)
|
.Where(i => i.ExtraType != null)
|
||||||
.Where(i => baseNames.Any(b =>
|
.Where(i => baseNames.Any(b =>
|
||||||
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
|
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
|
@ -8,7 +8,6 @@ using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Common.Updates;
|
using MediaBrowser.Common.Updates;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
using MediaBrowser.Controller.Devices;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
|
@ -30,7 +29,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ActivityLogEntryPoint : IServerEntryPoint
|
public sealed class ActivityLogEntryPoint : IServerEntryPoint
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger<ActivityLogEntryPoint> _logger;
|
||||||
private readonly IInstallationManager _installationManager;
|
private readonly IInstallationManager _installationManager;
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
private readonly ITaskManager _taskManager;
|
private readonly ITaskManager _taskManager;
|
||||||
|
@ -38,14 +37,12 @@ namespace Emby.Server.Implementations.Activity
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly ISubtitleManager _subManager;
|
private readonly ISubtitleManager _subManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IDeviceManager _deviceManager;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
|
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="sessionManager">The session manager.</param>
|
/// <param name="sessionManager">The session manager.</param>
|
||||||
/// <param name="deviceManager">The device manager.</param>
|
|
||||||
/// <param name="taskManager">The task manager.</param>
|
/// <param name="taskManager">The task manager.</param>
|
||||||
/// <param name="activityManager">The activity manager.</param>
|
/// <param name="activityManager">The activity manager.</param>
|
||||||
/// <param name="localization">The localization manager.</param>
|
/// <param name="localization">The localization manager.</param>
|
||||||
|
@ -55,7 +52,6 @@ namespace Emby.Server.Implementations.Activity
|
||||||
public ActivityLogEntryPoint(
|
public ActivityLogEntryPoint(
|
||||||
ILogger<ActivityLogEntryPoint> logger,
|
ILogger<ActivityLogEntryPoint> logger,
|
||||||
ISessionManager sessionManager,
|
ISessionManager sessionManager,
|
||||||
IDeviceManager deviceManager,
|
|
||||||
ITaskManager taskManager,
|
ITaskManager taskManager,
|
||||||
IActivityManager activityManager,
|
IActivityManager activityManager,
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
|
@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_deviceManager = deviceManager;
|
|
||||||
_taskManager = taskManager;
|
_taskManager = taskManager;
|
||||||
_activityManager = activityManager;
|
_activityManager = activityManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
|
@ -98,36 +93,18 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_userManager.OnUserDeleted += OnUserDeleted;
|
_userManager.OnUserDeleted += OnUserDeleted;
|
||||||
_userManager.OnUserLockedOut += OnUserLockedOut;
|
_userManager.OnUserLockedOut += OnUserLockedOut;
|
||||||
|
|
||||||
_deviceManager.CameraImageUploaded += OnCameraImageUploaded;
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
|
|
||||||
{
|
|
||||||
await CreateLogEntry(new ActivityLog(
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
_localization.GetLocalizedString("CameraImageUploadedFrom"),
|
|
||||||
e.Argument.Device.Name),
|
|
||||||
NotificationType.CameraImageUploaded.ToString(),
|
|
||||||
Guid.Empty,
|
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
|
private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
|
||||||
{
|
{
|
||||||
await CreateLogEntry(new ActivityLog(
|
await CreateLogEntry(new ActivityLog(
|
||||||
string.Format(
|
string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
_localization.GetLocalizedString("UserLockedOutWithName"),
|
_localization.GetLocalizedString("UserLockedOutWithName"),
|
||||||
e.Argument.Username),
|
e.Argument.Username),
|
||||||
NotificationType.UserLockedOut.ToString(),
|
NotificationType.UserLockedOut.ToString(),
|
||||||
e.Argument.Id,
|
e.Argument.Id))
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace))
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,11 +115,9 @@ namespace Emby.Server.Implementations.Activity
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
|
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
|
||||||
e.Provider,
|
e.Provider,
|
||||||
Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)),
|
Notifications.NotificationEntryPoint.GetItemName(e.Item)),
|
||||||
"SubtitleDownloadFailure",
|
"SubtitleDownloadFailure",
|
||||||
Guid.Empty,
|
Guid.Empty)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace)
|
|
||||||
{
|
{
|
||||||
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
|
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||||
ShortOverview = e.Exception.Message
|
ShortOverview = e.Exception.Message
|
||||||
|
@ -180,9 +155,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
GetItemName(item),
|
GetItemName(item),
|
||||||
e.DeviceName),
|
e.DeviceName),
|
||||||
GetPlaybackStoppedNotificationType(item.MediaType),
|
GetPlaybackStoppedNotificationType(item.MediaType),
|
||||||
user.Id,
|
user.Id))
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace))
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,9 +190,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
GetItemName(item),
|
GetItemName(item),
|
||||||
e.DeviceName),
|
e.DeviceName),
|
||||||
GetPlaybackNotificationType(item.MediaType),
|
GetPlaybackNotificationType(item.MediaType),
|
||||||
user.Id,
|
user.Id))
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace))
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,9 +257,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
session.UserName,
|
session.UserName,
|
||||||
session.DeviceName),
|
session.DeviceName),
|
||||||
"SessionEnded",
|
"SessionEnded",
|
||||||
session.UserId,
|
session.UserId)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace)
|
|
||||||
{
|
{
|
||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
|
@ -307,9 +276,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
|
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
|
||||||
user.Name),
|
user.Name),
|
||||||
"AuthenticationSucceeded",
|
"AuthenticationSucceeded",
|
||||||
user.Id,
|
user.Id)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace)
|
|
||||||
{
|
{
|
||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
|
@ -326,10 +293,9 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
|
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
|
||||||
e.Argument.Username),
|
e.Argument.Username),
|
||||||
"AuthenticationFailed",
|
"AuthenticationFailed",
|
||||||
Guid.Empty,
|
Guid.Empty)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Error)
|
|
||||||
{
|
{
|
||||||
|
LogSeverity = LogLevel.Error,
|
||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||||
|
@ -345,9 +311,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("UserPolicyUpdatedWithName"),
|
_localization.GetLocalizedString("UserPolicyUpdatedWithName"),
|
||||||
e.Argument.Username),
|
e.Argument.Username),
|
||||||
"UserPolicyUpdated",
|
"UserPolicyUpdated",
|
||||||
e.Argument.Id,
|
e.Argument.Id))
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace))
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,9 +323,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("UserDeletedWithName"),
|
_localization.GetLocalizedString("UserDeletedWithName"),
|
||||||
e.Argument.Username),
|
e.Argument.Username),
|
||||||
"UserDeleted",
|
"UserDeleted",
|
||||||
Guid.Empty,
|
Guid.Empty))
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace))
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,9 +335,8 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("UserPasswordChangedWithName"),
|
_localization.GetLocalizedString("UserPasswordChangedWithName"),
|
||||||
e.Argument.Username),
|
e.Argument.Username),
|
||||||
"UserPasswordChanged",
|
"UserPasswordChanged",
|
||||||
e.Argument.Id,
|
e.Argument.Id))
|
||||||
DateTime.UtcNow,
|
.ConfigureAwait(false);
|
||||||
LogLevel.Trace)).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnUserCreated(object sender, GenericEventArgs<User> e)
|
private async void OnUserCreated(object sender, GenericEventArgs<User> e)
|
||||||
|
@ -386,9 +347,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("UserCreatedWithName"),
|
_localization.GetLocalizedString("UserCreatedWithName"),
|
||||||
e.Argument.Username),
|
e.Argument.Username),
|
||||||
"UserCreated",
|
"UserCreated",
|
||||||
e.Argument.Id,
|
e.Argument.Id))
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace))
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,9 +367,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
session.UserName,
|
session.UserName,
|
||||||
session.DeviceName),
|
session.DeviceName),
|
||||||
"SessionStarted",
|
"SessionStarted",
|
||||||
session.UserId,
|
session.UserId)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace)
|
|
||||||
{
|
{
|
||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
|
@ -427,9 +384,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("PluginUpdatedWithName"),
|
_localization.GetLocalizedString("PluginUpdatedWithName"),
|
||||||
e.Argument.Item1.Name),
|
e.Argument.Item1.Name),
|
||||||
NotificationType.PluginUpdateInstalled.ToString(),
|
NotificationType.PluginUpdateInstalled.ToString(),
|
||||||
Guid.Empty,
|
Guid.Empty)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace)
|
|
||||||
{
|
{
|
||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
|
@ -447,9 +402,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("PluginUninstalledWithName"),
|
_localization.GetLocalizedString("PluginUninstalledWithName"),
|
||||||
e.Argument.Name),
|
e.Argument.Name),
|
||||||
NotificationType.PluginUninstalled.ToString(),
|
NotificationType.PluginUninstalled.ToString(),
|
||||||
Guid.Empty,
|
Guid.Empty))
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace))
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,9 +414,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("PluginInstalledWithName"),
|
_localization.GetLocalizedString("PluginInstalledWithName"),
|
||||||
e.Argument.name),
|
e.Argument.name),
|
||||||
NotificationType.PluginInstalled.ToString(),
|
NotificationType.PluginInstalled.ToString(),
|
||||||
Guid.Empty,
|
Guid.Empty)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace)
|
|
||||||
{
|
{
|
||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
|
@ -482,9 +433,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_localization.GetLocalizedString("NameInstallFailed"),
|
_localization.GetLocalizedString("NameInstallFailed"),
|
||||||
installationInfo.Name),
|
installationInfo.Name),
|
||||||
NotificationType.InstallationFailed.ToString(),
|
NotificationType.InstallationFailed.ToString(),
|
||||||
Guid.Empty,
|
Guid.Empty)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace)
|
|
||||||
{
|
{
|
||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
|
@ -528,10 +477,9 @@ namespace Emby.Server.Implementations.Activity
|
||||||
await CreateLogEntry(new ActivityLog(
|
await CreateLogEntry(new ActivityLog(
|
||||||
string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
|
string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
|
||||||
NotificationType.TaskFailed.ToString(),
|
NotificationType.TaskFailed.ToString(),
|
||||||
Guid.Empty,
|
Guid.Empty)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Error)
|
|
||||||
{
|
{
|
||||||
|
LogSeverity = LogLevel.Error,
|
||||||
Overview = string.Join(Environment.NewLine, vals),
|
Overview = string.Join(Environment.NewLine, vals),
|
||||||
ShortOverview = runningTime
|
ShortOverview = runningTime
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
@ -565,8 +513,6 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
|
_userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
|
||||||
_userManager.OnUserDeleted -= OnUserDeleted;
|
_userManager.OnUserDeleted -= OnUserDeleted;
|
||||||
_userManager.OnUserLockedOut -= OnUserLockedOut;
|
_userManager.OnUserLockedOut -= OnUserLockedOut;
|
||||||
|
|
||||||
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -586,7 +532,7 @@ namespace Emby.Server.Implementations.Activity
|
||||||
{
|
{
|
||||||
int years = days / DaysInYear;
|
int years = days / DaysInYear;
|
||||||
values.Add(CreateValueString(years, "year"));
|
values.Add(CreateValueString(years, "year"));
|
||||||
days = days % DaysInYear;
|
days %= DaysInYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number of months
|
// Number of months
|
||||||
|
|
|
@ -43,12 +43,8 @@ using Emby.Server.Implementations.Security;
|
||||||
using Emby.Server.Implementations.Serialization;
|
using Emby.Server.Implementations.Serialization;
|
||||||
using Emby.Server.Implementations.Services;
|
using Emby.Server.Implementations.Services;
|
||||||
using Emby.Server.Implementations.Session;
|
using Emby.Server.Implementations.Session;
|
||||||
using Emby.Server.Implementations.SocketSharp;
|
|
||||||
using Emby.Server.Implementations.TV;
|
using Emby.Server.Implementations.TV;
|
||||||
using Emby.Server.Implementations.Updates;
|
using Emby.Server.Implementations.Updates;
|
||||||
using Jellyfin.Server.Implementations;
|
|
||||||
using Jellyfin.Server.Implementations.Activity;
|
|
||||||
using Jellyfin.Server.Implementations.User;
|
|
||||||
using MediaBrowser.Api;
|
using MediaBrowser.Api;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
@ -84,7 +80,6 @@ using MediaBrowser.Controller.Subtitles;
|
||||||
using MediaBrowser.Controller.TV;
|
using MediaBrowser.Controller.TV;
|
||||||
using MediaBrowser.LocalMetadata.Savers;
|
using MediaBrowser.LocalMetadata.Savers;
|
||||||
using MediaBrowser.MediaEncoding.BdInfo;
|
using MediaBrowser.MediaEncoding.BdInfo;
|
||||||
using MediaBrowser.Model.Activity;
|
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
@ -103,12 +98,10 @@ using MediaBrowser.Providers.Subtitles;
|
||||||
using MediaBrowser.WebDashboard.Api;
|
using MediaBrowser.WebDashboard.Api;
|
||||||
using MediaBrowser.XbmcMetadata.Providers;
|
using MediaBrowser.XbmcMetadata.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
|
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
|
@ -505,32 +498,8 @@ namespace Emby.Server.Implementations
|
||||||
RegisterServices(serviceCollection);
|
RegisterServices(serviceCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
|
public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
|
||||||
{
|
=> _httpServer.RequestHandler(context);
|
||||||
if (!context.WebSockets.IsWebSocketRequest)
|
|
||||||
{
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _httpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
|
|
||||||
{
|
|
||||||
if (context.WebSockets.IsWebSocketRequest)
|
|
||||||
{
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = context.Request;
|
|
||||||
var response = context.Response;
|
|
||||||
var localPath = context.Request.Path.ToString();
|
|
||||||
|
|
||||||
var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
|
|
||||||
await _httpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers services/resources with the service collection that will be available via DI.
|
/// Registers services/resources with the service collection that will be available via DI.
|
||||||
|
@ -548,20 +517,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
||||||
|
|
||||||
// TODO: Remove support for injecting ILogger completely
|
|
||||||
serviceCollection.AddSingleton((provider) =>
|
|
||||||
{
|
|
||||||
Logger.LogWarning("Injecting ILogger directly is deprecated and should be replaced with ILogger<T>");
|
|
||||||
return Logger;
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: properly set up scoping and switch to AddDbContextPool
|
|
||||||
serviceCollection.AddDbContext<JellyfinDb>(
|
|
||||||
options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
|
|
||||||
ServiceLifetime.Transient);
|
|
||||||
|
|
||||||
serviceCollection.AddSingleton<JellyfinDbProvider>();
|
|
||||||
|
|
||||||
serviceCollection.AddSingleton(_fileSystemManager);
|
serviceCollection.AddSingleton(_fileSystemManager);
|
||||||
serviceCollection.AddSingleton<TvdbClientManager>();
|
serviceCollection.AddSingleton<TvdbClientManager>();
|
||||||
|
|
||||||
|
@ -599,6 +554,8 @@ namespace Emby.Server.Implementations
|
||||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||||
|
|
||||||
|
serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||||
|
@ -626,7 +583,6 @@ namespace Emby.Server.Implementations
|
||||||
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<ServiceController>();
|
serviceCollection.AddSingleton<ServiceController>();
|
||||||
serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
|
|
||||||
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
|
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
|
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
|
||||||
|
@ -668,8 +624,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
|
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
|
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
|
||||||
serviceCollection.AddSingleton<ISessionContext, SessionContext>();
|
serviceCollection.AddSingleton<ISessionContext, SessionContext>();
|
||||||
|
|
||||||
|
@ -697,6 +651,7 @@ namespace Emby.Server.Implementations
|
||||||
_httpServer = Resolve<IHttpServer>();
|
_httpServer = Resolve<IHttpServer>();
|
||||||
_httpClient = Resolve<IHttpClient>();
|
_httpClient = Resolve<IHttpClient>();
|
||||||
|
|
||||||
|
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
|
||||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||||
|
|
||||||
SetStaticProperties();
|
SetStaticProperties();
|
||||||
|
@ -1147,9 +1102,6 @@ namespace Emby.Server.Implementations
|
||||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
||||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
||||||
CachePath = ApplicationPaths.CachePath,
|
CachePath = ApplicationPaths.CachePath,
|
||||||
HttpServerPortNumber = HttpPort,
|
|
||||||
SupportsHttps = SupportsHttps,
|
|
||||||
HttpsPortNumber = HttpsPort,
|
|
||||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
OperatingSystem = OperatingSystem.Id.ToString(),
|
||||||
OperatingSystemDisplayName = OperatingSystem.Name,
|
OperatingSystemDisplayName = OperatingSystem.Name,
|
||||||
CanSelfRestart = CanSelfRestart,
|
CanSelfRestart = CanSelfRestart,
|
||||||
|
@ -1185,23 +1137,22 @@ namespace Emby.Server.Implementations
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps;
|
/// <inheritdoc/>
|
||||||
|
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
|
||||||
|
|
||||||
public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy;
|
/// <inheritdoc/>
|
||||||
|
public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken)
|
||||||
public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Return the first matched address, if found, or the first known local address
|
// Return the first matched address, if found, or the first known local address
|
||||||
var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
|
var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (addresses.Count == 0)
|
||||||
foreach (var address in addresses)
|
|
||||||
{
|
{
|
||||||
return GetLocalApiUrl(address, forceHttp);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return GetLocalApiUrl(addresses.First());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -1228,7 +1179,7 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetLocalApiUrl(IPAddress ipAddress, bool forceHttp = false)
|
public string GetLocalApiUrl(IPAddress ipAddress)
|
||||||
{
|
{
|
||||||
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
{
|
{
|
||||||
|
@ -1238,29 +1189,30 @@ namespace Emby.Server.Implementations
|
||||||
str.CopyTo(span.Slice(1));
|
str.CopyTo(span.Slice(1));
|
||||||
span[^1] = ']';
|
span[^1] = ']';
|
||||||
|
|
||||||
return GetLocalApiUrl(span, forceHttp);
|
return GetLocalApiUrl(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetLocalApiUrl(ipAddress.ToString(), forceHttp);
|
return GetLocalApiUrl(ipAddress.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc/>
|
||||||
public string GetLocalApiUrl(ReadOnlySpan<char> host, bool forceHttp = false)
|
public string GetLoopbackHttpApiUrl()
|
||||||
{
|
{
|
||||||
var url = new StringBuilder(64);
|
return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
|
||||||
bool useHttps = EnableHttps && !forceHttp;
|
}
|
||||||
url.Append(useHttps ? "https://" : "http://")
|
|
||||||
.Append(host)
|
|
||||||
.Append(':')
|
|
||||||
.Append(useHttps ? HttpsPort : HttpPort);
|
|
||||||
|
|
||||||
string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
|
/// <inheritdoc/>
|
||||||
if (baseUrl.Length != 0)
|
public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null)
|
||||||
|
{
|
||||||
|
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
||||||
|
// not. For consistency, always trim the trailing slash.
|
||||||
|
return new UriBuilder
|
||||||
{
|
{
|
||||||
url.Append(baseUrl);
|
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||||
}
|
Host = host.ToString(),
|
||||||
|
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||||
return url.ToString();
|
Path = ServerConfigurationManager.Configuration.BaseUrl
|
||||||
|
}.ToString().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
|
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
|
||||||
|
@ -1294,7 +1246,7 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
|
var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
|
||||||
if (valid)
|
if (valid)
|
||||||
{
|
{
|
||||||
resultList.Add(address);
|
resultList.Add(address);
|
||||||
|
@ -1328,7 +1280,7 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
|
private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (address.Equals(IPAddress.Loopback)
|
if (address.Equals(IPAddress.Loopback)
|
||||||
|| address.Equals(IPAddress.IPv6Loopback))
|
|| address.Equals(IPAddress.IPv6Loopback))
|
||||||
|
@ -1336,8 +1288,7 @@ namespace Emby.Server.Implementations
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiUrl = GetLocalApiUrl(address);
|
var apiUrl = GetLocalApiUrl(address) + "/system/ping";
|
||||||
apiUrl += "/system/ping";
|
|
||||||
|
|
||||||
if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
|
if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
|
||||||
{
|
{
|
||||||
|
|
|
@ -31,18 +31,18 @@ namespace Emby.Server.Implementations.Browser
|
||||||
/// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
|
/// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="appHost">The application host.</param>
|
/// <param name="appHost">The application host.</param>
|
||||||
/// <param name="url">The URL.</param>
|
/// <param name="relativeUrl">The URL to open, relative to the server base URL.</param>
|
||||||
private static void TryOpenUrl(IServerApplicationHost appHost, string url)
|
private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string baseUrl = appHost.GetLocalApiUrl("localhost");
|
string baseUrl = appHost.GetLocalApiUrl("localhost");
|
||||||
appHost.LaunchUrl(baseUrl + url);
|
appHost.LaunchUrl(baseUrl + relativeUrl);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var logger = appHost.Resolve<ILogger>();
|
var logger = appHost.Resolve<ILogger>();
|
||||||
logger?.LogError(ex, "Failed to open browser window with URL {URL}", url);
|
logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.CameraUploadUpgraded)
|
|
||||||
{
|
|
||||||
config.CameraUploadUpgraded = true;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.CollectionsUpgraded)
|
if (!config.CollectionsUpgraded)
|
||||||
{
|
{
|
||||||
config.CollectionsUpgraded = true;
|
config.CollectionsUpgraded = true;
|
||||||
|
|
|
@ -7,26 +7,19 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Controller.Security;
|
using MediaBrowser.Controller.Security;
|
||||||
using MediaBrowser.Model.Configuration;
|
|
||||||
using MediaBrowser.Model.Devices;
|
using MediaBrowser.Model.Devices;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Events;
|
using MediaBrowser.Model.Events;
|
||||||
using MediaBrowser.Model.Globalization;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
using MediaBrowser.Model.Users;
|
using MediaBrowser.Model.Users;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Devices
|
namespace Emby.Server.Implementations.Devices
|
||||||
{
|
{
|
||||||
|
@ -34,38 +27,23 @@ namespace Emby.Server.Implementations.Devices
|
||||||
{
|
{
|
||||||
private readonly IJsonSerializer _json;
|
private readonly IJsonSerializer _json;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly ILibraryMonitor _libraryMonitor;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ILocalizationManager _localizationManager;
|
|
||||||
private readonly IAuthenticationRepository _authRepo;
|
private readonly IAuthenticationRepository _authRepo;
|
||||||
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
|
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
|
||||||
|
|
||||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||||
|
|
||||||
public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
|
|
||||||
|
|
||||||
private readonly object _cameraUploadSyncLock = new object();
|
|
||||||
private readonly object _capabilitiesSyncLock = new object();
|
private readonly object _capabilitiesSyncLock = new object();
|
||||||
|
|
||||||
public DeviceManager(
|
public DeviceManager(
|
||||||
IAuthenticationRepository authRepo,
|
IAuthenticationRepository authRepo,
|
||||||
IJsonSerializer json,
|
IJsonSerializer json,
|
||||||
ILibraryManager libraryManager,
|
|
||||||
ILocalizationManager localizationManager,
|
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IFileSystem fileSystem,
|
|
||||||
ILibraryMonitor libraryMonitor,
|
|
||||||
IServerConfigurationManager config)
|
IServerConfigurationManager config)
|
||||||
{
|
{
|
||||||
_json = json;
|
_json = json;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_libraryMonitor = libraryMonitor;
|
|
||||||
_config = config;
|
_config = config;
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_localizationManager = localizationManager;
|
|
||||||
_authRepo = authRepo;
|
_authRepo = authRepo;
|
||||||
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
|
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
@ -195,173 +173,7 @@ namespace Emby.Server.Implementations.Devices
|
||||||
return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
|
return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentUploadHistory GetCameraUploadHistory(string deviceId)
|
public bool CanAccessDevice(User user, string deviceId)
|
||||||
{
|
|
||||||
var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
|
|
||||||
|
|
||||||
lock (_cameraUploadSyncLock)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _json.DeserializeFromFile<ContentUploadHistory>(path);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
return new ContentUploadHistory
|
|
||||||
{
|
|
||||||
DeviceId = deviceId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file)
|
|
||||||
{
|
|
||||||
var device = GetDevice(deviceId, false);
|
|
||||||
var uploadPathInfo = GetUploadPath(device);
|
|
||||||
|
|
||||||
var path = uploadPathInfo.Item1;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(file.Album))
|
|
||||||
{
|
|
||||||
path = Path.Combine(path, _fileSystem.GetValidFilename(file.Album));
|
|
||||||
}
|
|
||||||
|
|
||||||
path = Path.Combine(path, file.Name);
|
|
||||||
path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
||||||
|
|
||||||
await EnsureLibraryFolder(uploadPathInfo.Item2, uploadPathInfo.Item3).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_libraryMonitor.ReportFileSystemChangeBeginning(path);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
|
||||||
{
|
|
||||||
await stream.CopyToAsync(fs).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddCameraUpload(deviceId, file);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_libraryMonitor.ReportFileSystemChangeComplete(path, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CameraImageUploaded != null)
|
|
||||||
{
|
|
||||||
CameraImageUploaded?.Invoke(this, new GenericEventArgs<CameraImageUploadInfo>
|
|
||||||
{
|
|
||||||
Argument = new CameraImageUploadInfo
|
|
||||||
{
|
|
||||||
Device = device,
|
|
||||||
FileInfo = file
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddCameraUpload(string deviceId, LocalFileInfo file)
|
|
||||||
{
|
|
||||||
var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
||||||
|
|
||||||
lock (_cameraUploadSyncLock)
|
|
||||||
{
|
|
||||||
ContentUploadHistory history;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
history = _json.DeserializeFromFile<ContentUploadHistory>(path);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
history = new ContentUploadHistory
|
|
||||||
{
|
|
||||||
DeviceId = deviceId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
history.DeviceId = deviceId;
|
|
||||||
|
|
||||||
var list = history.FilesUploaded.ToList();
|
|
||||||
list.Add(file);
|
|
||||||
history.FilesUploaded = list.ToArray();
|
|
||||||
|
|
||||||
_json.SerializeToFile(history, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal Task EnsureLibraryFolder(string path, string name)
|
|
||||||
{
|
|
||||||
var existingFolders = _libraryManager
|
|
||||||
.RootFolder
|
|
||||||
.Children
|
|
||||||
.OfType<Folder>()
|
|
||||||
.Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (existingFolders.Count > 0)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
|
|
||||||
var libraryOptions = new LibraryOptions
|
|
||||||
{
|
|
||||||
PathInfos = new[] { new MediaPathInfo { Path = path } },
|
|
||||||
EnablePhotos = true,
|
|
||||||
EnableRealtimeMonitor = false,
|
|
||||||
SaveLocalMetadata = true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
name = _localizationManager.GetLocalizedString("HeaderCameraUploads");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _libraryManager.AddVirtualFolder(name, CollectionType.HomeVideos, libraryOptions, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Tuple<string, string, string> GetUploadPath(DeviceInfo device)
|
|
||||||
{
|
|
||||||
var config = _config.GetUploadOptions();
|
|
||||||
var path = config.CameraUploadPath;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
path = DefaultCameraUploadsPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
var topLibraryPath = path;
|
|
||||||
|
|
||||||
if (config.EnableCameraUploadSubfolders)
|
|
||||||
{
|
|
||||||
path = Path.Combine(path, _fileSystem.GetValidFilename(device.Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Tuple<string, string, string>(path, topLibraryPath, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal string GetUploadsPath()
|
|
||||||
{
|
|
||||||
var config = _config.GetUploadOptions();
|
|
||||||
var path = config.CameraUploadPath;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
path = DefaultCameraUploadsPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string DefaultCameraUploadsPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads");
|
|
||||||
|
|
||||||
public bool CanAccessDevice(Jellyfin.Data.Entities.User user, string deviceId)
|
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
|
@ -391,102 +203,4 @@ namespace Emby.Server.Implementations.Devices
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DeviceManagerEntryPoint : IServerEntryPoint
|
|
||||||
{
|
|
||||||
private readonly DeviceManager _deviceManager;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
private ILogger _logger;
|
|
||||||
|
|
||||||
public DeviceManagerEntryPoint(
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IServerConfigurationManager config,
|
|
||||||
ILogger<DeviceManagerEntryPoint> logger)
|
|
||||||
{
|
|
||||||
_deviceManager = (DeviceManager)deviceManager;
|
|
||||||
_config = config;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RunAsync()
|
|
||||||
{
|
|
||||||
if (!_config.Configuration.CameraUploadUpgraded && _config.Configuration.IsStartupWizardCompleted)
|
|
||||||
{
|
|
||||||
var path = _deviceManager.GetUploadsPath();
|
|
||||||
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _deviceManager.EnsureLibraryFolder(path, null).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error creating camera uploads library");
|
|
||||||
}
|
|
||||||
|
|
||||||
_config.Configuration.CameraUploadUpgraded = true;
|
|
||||||
_config.SaveConfiguration();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IDisposable Support
|
|
||||||
private bool disposedValue = false; // To detect redundant calls
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!disposedValue)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
// TODO: dispose managed state (managed objects).
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
|
|
||||||
// TODO: set large fields to null.
|
|
||||||
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
|
|
||||||
// ~DeviceManagerEntryPoint() {
|
|
||||||
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
|
||||||
// Dispose(false);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// This code added to correctly implement the disposable pattern.
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
|
||||||
Dispose(true);
|
|
||||||
// TODO: uncomment the following line if the finalizer is overridden above.
|
|
||||||
// GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DevicesConfigStore : IConfigurationFactory
|
|
||||||
{
|
|
||||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
|
||||||
{
|
|
||||||
return new ConfigurationStore[]
|
|
||||||
{
|
|
||||||
new ConfigurationStore
|
|
||||||
{
|
|
||||||
Key = "devices",
|
|
||||||
ConfigurationType = typeof(DevicesOptions)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UploadConfigExtension
|
|
||||||
{
|
|
||||||
public static DevicesOptions GetUploadOptions(this IConfigurationManager config)
|
|
||||||
{
|
|
||||||
return config.GetConfiguration<DevicesOptions>("devices");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
|
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
|
||||||
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
|
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
|
||||||
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
|
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
|
||||||
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
|
@ -51,7 +50,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
.Append(config.PublicHttpsPort).Append(Separator)
|
.Append(config.PublicHttpsPort).Append(Separator)
|
||||||
.Append(_appHost.HttpPort).Append(Separator)
|
.Append(_appHost.HttpPort).Append(Separator)
|
||||||
.Append(_appHost.HttpsPort).Append(Separator)
|
.Append(_appHost.HttpsPort).Append(Separator)
|
||||||
.Append(_appHost.EnableHttps).Append(Separator)
|
.Append(_appHost.ListenWithHttps).Append(Separator)
|
||||||
.Append(config.EnableRemoteAccess).Append(Separator)
|
.Append(config.EnableRemoteAccess).Append(Separator)
|
||||||
.ToString();
|
.ToString();
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
{
|
{
|
||||||
yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
|
yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
|
||||||
|
|
||||||
if (_appHost.EnableHttps)
|
if (_appHost.ListenWithHttps)
|
||||||
{
|
{
|
||||||
yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
|
yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,12 @@ using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Net.WebSockets;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.Net;
|
|
||||||
using Emby.Server.Implementations.Services;
|
using Emby.Server.Implementations.Services;
|
||||||
|
using Emby.Server.Implementations.SocketSharp;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
@ -22,15 +23,17 @@ using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
using ServiceStack.Text.Jsv;
|
using ServiceStack.Text.Jsv;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer
|
namespace Emby.Server.Implementations.HttpServer
|
||||||
{
|
{
|
||||||
public class HttpListenerHost : IHttpServer, IDisposable
|
public class HttpListenerHost : IHttpServer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The key for a setting that specifies the default redirect path
|
/// The key for a setting that specifies the default redirect path
|
||||||
|
@ -39,17 +42,17 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
|
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
private readonly IHttpListener _socketListener;
|
|
||||||
private readonly Func<Type, Func<string, object>> _funcParseFn;
|
private readonly Func<Type, Func<string, object>> _funcParseFn;
|
||||||
private readonly string _defaultRedirectPath;
|
private readonly string _defaultRedirectPath;
|
||||||
private readonly string _baseUrlPrefix;
|
private readonly string _baseUrlPrefix;
|
||||||
|
|
||||||
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
|
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
|
||||||
private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
|
|
||||||
private readonly IHostEnvironment _hostEnvironment;
|
private readonly IHostEnvironment _hostEnvironment;
|
||||||
|
|
||||||
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
|
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
|
||||||
|
@ -63,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
INetworkManager networkManager,
|
INetworkManager networkManager,
|
||||||
IJsonSerializer jsonSerializer,
|
IJsonSerializer jsonSerializer,
|
||||||
IXmlSerializer xmlSerializer,
|
IXmlSerializer xmlSerializer,
|
||||||
IHttpListener socketListener,
|
|
||||||
ILocalizationManager localizationManager,
|
ILocalizationManager localizationManager,
|
||||||
ServiceController serviceController,
|
ServiceController serviceController,
|
||||||
IHostEnvironment hostEnvironment)
|
IHostEnvironment hostEnvironment,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_appHost = applicationHost;
|
_appHost = applicationHost;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
@ -76,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
_networkManager = networkManager;
|
_networkManager = networkManager;
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_xmlSerializer = xmlSerializer;
|
_xmlSerializer = xmlSerializer;
|
||||||
_socketListener = socketListener;
|
|
||||||
ServiceController = serviceController;
|
ServiceController = serviceController;
|
||||||
|
|
||||||
_socketListener.WebSocketConnected = OnWebSocketConnected;
|
|
||||||
_hostEnvironment = hostEnvironment;
|
_hostEnvironment = hostEnvironment;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
|
||||||
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
|
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
|
||||||
|
|
||||||
|
@ -172,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWebSocketConnected(WebSocketConnectEventArgs e)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
|
|
||||||
{
|
|
||||||
OnReceive = ProcessWebSocketMessageReceived,
|
|
||||||
Url = e.Url,
|
|
||||||
QueryString = e.QueryString
|
|
||||||
};
|
|
||||||
|
|
||||||
connection.Closed += OnConnectionClosed;
|
|
||||||
|
|
||||||
lock (_webSocketConnections)
|
|
||||||
{
|
|
||||||
_webSocketConnections.Add(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnConnectionClosed(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
lock (_webSocketConnections)
|
|
||||||
{
|
|
||||||
_webSocketConnections.Remove((IWebSocketConnection)sender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Exception GetActualException(Exception ex)
|
private static Exception GetActualException(Exception ex)
|
||||||
{
|
{
|
||||||
if (ex is AggregateException agg)
|
if (ex is AggregateException agg)
|
||||||
|
@ -289,32 +258,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
|
.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shut down the Web Service
|
|
||||||
/// </summary>
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
List<IWebSocketConnection> connections;
|
|
||||||
|
|
||||||
lock (_webSocketConnections)
|
|
||||||
{
|
|
||||||
connections = _webSocketConnections.ToList();
|
|
||||||
_webSocketConnections.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var connection in connections)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
connection.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error disposing connection");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string RemoveQueryStringByKey(string url, string key)
|
public static string RemoveQueryStringByKey(string url, string key)
|
||||||
{
|
{
|
||||||
var uri = new Uri(url);
|
var uri = new Uri(url);
|
||||||
|
@ -424,33 +367,52 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
|
||||||
private bool ValidateSsl(string remoteIp, string urlString)
|
private bool ValidateSsl(string remoteIp, string urlString)
|
||||||
{
|
{
|
||||||
if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy)
|
if (_config.Configuration.RequireHttps
|
||||||
|
&& _appHost.ListenWithHttps
|
||||||
|
&& !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
|
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
||||||
|
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|
||||||
|
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||||
{
|
{
|
||||||
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
return true;
|
||||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|
}
|
||||||
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_networkManager.IsInLocalNetwork(remoteIp))
|
if (!_networkManager.IsInLocalNetwork(remoteIp))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task RequestHandler(HttpContext context)
|
||||||
|
{
|
||||||
|
if (context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
return WebSocketRequestHandler(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = context.Request;
|
||||||
|
var response = context.Response;
|
||||||
|
var localPath = context.Request.Path.ToString();
|
||||||
|
|
||||||
|
var req = new WebSocketSharpRequest(request, response, request.Path, _logger);
|
||||||
|
return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Overridable method that can be used to implement a custom handler.
|
/// Overridable method that can be used to implement a custom handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
|
private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var stopWatch = new Stopwatch();
|
var stopWatch = new Stopwatch();
|
||||||
stopWatch.Start();
|
stopWatch.Start();
|
||||||
|
@ -493,9 +455,10 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
httpRes.StatusCode = 200;
|
httpRes.StatusCode = 200;
|
||||||
httpRes.Headers.Add("Access-Control-Allow-Origin", "*");
|
foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||||
httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
{
|
||||||
httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
|
httpRes.Headers.Add(key, value);
|
||||||
|
}
|
||||||
httpRes.ContentType = "text/plain";
|
httpRes.ContentType = "text/plain";
|
||||||
await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
|
await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
|
@ -578,6 +541,68 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task WebSocketRequestHandler(HttpContext context)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
||||||
|
|
||||||
|
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
var connection = new WebSocketConnection(
|
||||||
|
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||||
|
webSocket,
|
||||||
|
context.Connection.RemoteIpAddress,
|
||||||
|
context.Request.Query)
|
||||||
|
{
|
||||||
|
OnReceive = ProcessWebSocketMessageReceived
|
||||||
|
};
|
||||||
|
|
||||||
|
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
|
||||||
|
|
||||||
|
await connection.ProcessAsync().ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
|
||||||
|
}
|
||||||
|
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
|
||||||
|
if (!context.Response.HasStarted)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the default CORS headers
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="req"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
|
||||||
|
{
|
||||||
|
var origin = req.Headers["Origin"];
|
||||||
|
if (origin == StringValues.Empty)
|
||||||
|
{
|
||||||
|
origin = req.Headers["Host"];
|
||||||
|
if (origin == StringValues.Empty)
|
||||||
|
{
|
||||||
|
origin = "*";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
headers.Add("Access-Control-Allow-Origin", origin);
|
||||||
|
headers.Add("Access-Control-Allow-Credentials", "true");
|
||||||
|
headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||||
|
headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
// Entry point for HttpListener
|
// Entry point for HttpListener
|
||||||
public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
|
public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
|
||||||
{
|
{
|
||||||
|
@ -624,7 +649,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
ResponseFilters = new Action<IRequest, HttpResponse, object>[]
|
ResponseFilters = new Action<IRequest, HttpResponse, object>[]
|
||||||
{
|
{
|
||||||
new ResponseFilter(_logger).FilterResponse
|
new ResponseFilter(this, _logger).FilterResponse
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -685,11 +710,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
|
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ProcessWebSocketRequest(HttpContext context)
|
|
||||||
{
|
|
||||||
return _socketListener.ProcessWebSocketRequest(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string NormalizeEmbyRoutePath(string path)
|
private string NormalizeEmbyRoutePath(string path)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Normalizing /emby route");
|
_logger.LogDebug("Normalizing /emby route");
|
||||||
|
@ -708,28 +728,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
return _baseUrlPrefix + NormalizeUrlPath(path);
|
return _baseUrlPrefix + NormalizeUrlPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes the web socket message received.
|
/// Processes the web socket message received.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -741,8 +739,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Websocket message received: {0}", result.MessageType);
|
|
||||||
|
|
||||||
IEnumerable<Task> GetTasks()
|
IEnumerable<Task> GetTasks()
|
||||||
{
|
{
|
||||||
foreach (var x in _webSocketListeners)
|
foreach (var x in _webSocketListeners)
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Server.Implementations.Net;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer
|
|
||||||
{
|
|
||||||
public interface IHttpListener : IDisposable
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the error handler.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The error handler.</value>
|
|
||||||
Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the request handler.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The request handler.</value>
|
|
||||||
Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the web socket handler.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The web socket handler.</value>
|
|
||||||
Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops this instance.
|
|
||||||
/// </summary>
|
|
||||||
Task Stop();
|
|
||||||
|
|
||||||
Task ProcessWebSocketRequest(HttpContext ctx);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -13,14 +15,17 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ResponseFilter
|
public class ResponseFilter
|
||||||
{
|
{
|
||||||
|
private readonly IHttpServer _server;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ResponseFilter"/> class.
|
/// Initializes a new instance of the <see cref="ResponseFilter"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="server">The HTTP server.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
public ResponseFilter(ILogger logger)
|
public ResponseFilter(IHttpServer server, ILogger logger)
|
||||||
{
|
{
|
||||||
|
_server = server;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,10 +37,16 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// <param name="dto">The dto.</param>
|
/// <param name="dto">The dto.</param>
|
||||||
public void FilterResponse(IRequest req, HttpResponse res, object dto)
|
public void FilterResponse(IRequest req, HttpResponse res, object dto)
|
||||||
{
|
{
|
||||||
|
foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
|
||||||
|
{
|
||||||
|
res.Headers.Add(key, value);
|
||||||
|
}
|
||||||
// Try to prevent compatibility view
|
// Try to prevent compatibility view
|
||||||
res.Headers.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
|
res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
|
||||||
res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
"Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
|
||||||
res.Headers.Add("Access-Control-Allow-Origin", "*");
|
"Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
|
||||||
|
"Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
|
||||||
|
"X-Emby-Authorization");
|
||||||
|
|
||||||
if (dto is Exception exception)
|
if (dto is Exception exception)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
using System;
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.Net;
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using UtfUnknown;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer
|
namespace Emby.Server.Implementations.HttpServer
|
||||||
{
|
{
|
||||||
|
@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The json serializer.
|
/// The json serializer options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The socket.
|
/// The socket.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IWebSocket _socket;
|
private readonly WebSocket _socket;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="socket">The socket.</param>
|
/// <param name="socket">The socket.</param>
|
||||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||||
/// <param name="jsonSerializer">The json serializer.</param>
|
/// <param name="query">The query.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
public WebSocketConnection(
|
||||||
/// <exception cref="ArgumentNullException">socket</exception>
|
ILogger<WebSocketConnection> logger,
|
||||||
public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
|
WebSocket socket,
|
||||||
|
IPAddress? remoteEndPoint,
|
||||||
|
IQueryCollection query)
|
||||||
{
|
{
|
||||||
if (socket == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(socket));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(remoteEndPoint))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(remoteEndPoint));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonSerializer == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(jsonSerializer));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(logger));
|
|
||||||
}
|
|
||||||
|
|
||||||
Id = Guid.NewGuid();
|
|
||||||
_jsonSerializer = jsonSerializer;
|
|
||||||
_socket = socket;
|
|
||||||
_socket.OnReceiveBytes = OnReceiveInternal;
|
|
||||||
|
|
||||||
RemoteEndPoint = remoteEndPoint;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_socket = socket;
|
||||||
|
RemoteEndPoint = remoteEndPoint;
|
||||||
|
QueryString = query;
|
||||||
|
|
||||||
socket.Closed += OnSocketClosed;
|
_jsonOptions = JsonDefaults.GetOptions();
|
||||||
|
LastActivityDate = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event EventHandler<EventArgs> Closed;
|
public event EventHandler<EventArgs>? Closed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the remote end point.
|
/// Gets or sets the remote end point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string RemoteEndPoint { get; private set; }
|
public IPAddress? RemoteEndPoint { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the receive action.
|
/// Gets or sets the receive action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The receive action.</value>
|
/// <value>The receive action.</value>
|
||||||
public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
|
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the last activity date.
|
/// Gets the last activity date.
|
||||||
|
@ -94,23 +78,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// <value>The last activity date.</value>
|
/// <value>The last activity date.</value>
|
||||||
public DateTime LastActivityDate { get; private set; }
|
public DateTime LastActivityDate { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the id.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The id.</value>
|
|
||||||
public Guid Id { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the URL.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The URL.</value>
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the query string.
|
/// Gets or sets the query string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The query string.</value>
|
/// <value>The query string.</value>
|
||||||
public IQueryCollection QueryString { get; set; }
|
public IQueryCollection QueryString { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the state.
|
/// Gets the state.
|
||||||
|
@ -118,70 +90,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// <value>The state.</value>
|
/// <value>The state.</value>
|
||||||
public WebSocketState State => _socket.State;
|
public WebSocketState State => _socket.State;
|
||||||
|
|
||||||
void OnSocketClosed(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
Closed?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when [receive].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bytes">The bytes.</param>
|
|
||||||
private void OnReceiveInternal(byte[] bytes)
|
|
||||||
{
|
|
||||||
LastActivityDate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (OnReceive == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
|
|
||||||
|
|
||||||
if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnReceiveInternal(string message)
|
|
||||||
{
|
|
||||||
LastActivityDate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// This info is useful sometimes but also clogs up the log
|
|
||||||
_logger.LogDebug("Received web socket message that is not a json structure: {message}", message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OnReceive == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
|
|
||||||
|
|
||||||
var info = new WebSocketMessageInfo
|
|
||||||
{
|
|
||||||
MessageType = stub.MessageType,
|
|
||||||
Data = stub.Data?.ToString(),
|
|
||||||
Connection = this
|
|
||||||
};
|
|
||||||
|
|
||||||
OnReceive(info);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error processing web socket message");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a message asynchronously.
|
/// Sends a message asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -189,67 +97,128 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// <param name="message">The message.</param>
|
/// <param name="message">The message.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
/// <exception cref="ArgumentNullException">message</exception>
|
|
||||||
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (message == null)
|
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||||
{
|
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
||||||
throw new ArgumentNullException(nameof(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = _jsonSerializer.SerializeToString(message);
|
|
||||||
|
|
||||||
return SendAsync(json, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a message asynchronously.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buffer">The buffer.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (buffer == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
return _socket.SendAsync(buffer, true, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task SendAsync(string text, CancellationToken cancellationToken)
|
public async Task ProcessAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text))
|
var pipe = new Pipe();
|
||||||
|
var writer = pipe.Writer;
|
||||||
|
|
||||||
|
ValueWebSocketReceiveResult receiveresult;
|
||||||
|
do
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(text));
|
// Allocate at least 512 bytes from the PipeWriter
|
||||||
|
Memory<byte> memory = writer.GetMemory(512);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (WebSocketException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bytesRead = receiveresult.Count;
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the PipeWriter how much was read from the Socket
|
||||||
|
writer.Advance(bytesRead);
|
||||||
|
|
||||||
|
// Make the data available to the PipeReader
|
||||||
|
FlushResult flushResult = await writer.FlushAsync();
|
||||||
|
if (flushResult.IsCompleted)
|
||||||
|
{
|
||||||
|
// The PipeReader stopped reading
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LastActivityDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (receiveresult.EndOfMessage)
|
||||||
|
{
|
||||||
|
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
} while (
|
||||||
|
(_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
|
||||||
|
&& receiveresult.MessageType != WebSocketMessageType.Close);
|
||||||
|
|
||||||
|
Closed?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
if (_socket.State == WebSocketState.Open
|
||||||
|
|| _socket.State == WebSocketState.CloseReceived
|
||||||
|
|| _socket.State == WebSocketState.CloseSent)
|
||||||
|
{
|
||||||
|
await _socket.CloseAsync(
|
||||||
|
WebSocketCloseStatus.NormalClosure,
|
||||||
|
string.Empty,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
return _socket.SendAsync(text, true, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private async Task ProcessInternal(PipeReader reader)
|
||||||
public void Dispose()
|
|
||||||
{
|
{
|
||||||
Dispose(true);
|
ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
|
||||||
GC.SuppressFinalize(this);
|
ReadOnlySequence<byte> buffer = result.Buffer;
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (OnReceive == null)
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool dispose)
|
|
||||||
{
|
|
||||||
if (dispose)
|
|
||||||
{
|
{
|
||||||
_socket.Dispose();
|
// Tell the PipeReader how much of the buffer we have consumed
|
||||||
|
reader.AdvanceTo(buffer.End);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WebSocketMessage<object> stub;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
if (buffer.IsSingleSegment)
|
||||||
|
{
|
||||||
|
stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
buffer.CopyTo(buf);
|
||||||
|
stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
// Tell the PipeReader how much of the buffer we have consumed
|
||||||
|
reader.AdvanceTo(buffer.End);
|
||||||
|
_logger.LogError(ex, "Error processing web socket message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the PipeReader how much of the buffer we have consumed
|
||||||
|
reader.AdvanceTo(buffer.End);
|
||||||
|
|
||||||
|
_logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
|
||||||
|
|
||||||
|
var info = new WebSocketMessageInfo
|
||||||
|
{
|
||||||
|
MessageType = stub.MessageType,
|
||||||
|
Data = stub.Data?.ToString(), // Data can be null
|
||||||
|
Connection = this
|
||||||
|
};
|
||||||
|
|
||||||
|
await OnReceive(info).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
|
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="fileSystem">The file system.</param>
|
/// <param name="fileSystem">The file system.</param>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager)
|
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
|
@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
|
||||||
/// <param name="fileSystem">The file system.</param>
|
/// <param name="fileSystem">The file system.</param>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
/// <param name="config">The configuration manager.</param>
|
/// <param name="config">The configuration manager.</param>
|
||||||
public MusicArtistResolver(
|
public MusicArtistResolver(
|
||||||
ILogger<MusicArtistResolver> logger,
|
ILogger<MusicAlbumResolver> logger,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IServerConfigurationManager config)
|
IServerConfigurationManager config)
|
||||||
|
|
|
@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
{
|
{
|
||||||
var stream = new MediaSourceInfo
|
var stream = new MediaSourceInfo
|
||||||
{
|
{
|
||||||
EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
|
EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
|
||||||
EncoderProtocol = MediaProtocol.Http,
|
EncoderProtocol = MediaProtocol.Http,
|
||||||
Path = info.Path,
|
Path = info.Path,
|
||||||
Protocol = MediaProtocol.File,
|
Protocol = MediaProtocol.File,
|
||||||
|
|
|
@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
//OpenedMediaSource.Path = tempFile;
|
//OpenedMediaSource.Path = tempFile;
|
||||||
//OpenedMediaSource.ReadAtNativeFramerate = true;
|
//OpenedMediaSource.ReadAtNativeFramerate = true;
|
||||||
|
|
||||||
MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||||
MediaSource.Protocol = MediaProtocol.Http;
|
MediaSource.Protocol = MediaProtocol.Http;
|
||||||
//OpenedMediaSource.SupportsDirectPlay = false;
|
//OpenedMediaSource.SupportsDirectPlay = false;
|
||||||
//OpenedMediaSource.SupportsDirectStream = true;
|
//OpenedMediaSource.SupportsDirectStream = true;
|
||||||
|
|
|
@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
public M3UTunerHost(
|
public M3UTunerHost(
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
ILogger logger,
|
ILogger<M3UTunerHost> logger,
|
||||||
IJsonSerializer jsonSerializer,
|
IJsonSerializer jsonSerializer,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
|
@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
return Task.FromResult(list);
|
return Task.FromResult(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly string[] _disallowedSharedStreamExtensions = new string[]
|
private static readonly string[] _disallowedSharedStreamExtensions =
|
||||||
{
|
{
|
||||||
".mkv",
|
".mkv",
|
||||||
".mp4",
|
".mp4",
|
||||||
|
|
|
@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
//OpenedMediaSource.Path = tempFile;
|
//OpenedMediaSource.Path = tempFile;
|
||||||
//OpenedMediaSource.ReadAtNativeFramerate = true;
|
//OpenedMediaSource.ReadAtNativeFramerate = true;
|
||||||
|
|
||||||
MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||||
MediaSource.Protocol = MediaProtocol.Http;
|
MediaSource.Protocol = MediaProtocol.Http;
|
||||||
|
|
||||||
//OpenedMediaSource.Path = TempFilePath;
|
//OpenedMediaSource.Path = TempFilePath;
|
||||||
|
|
|
@ -91,5 +91,7 @@
|
||||||
"HeaderNextUp": "এরপরে আসছে",
|
"HeaderNextUp": "এরপরে আসছে",
|
||||||
"HeaderLiveTV": "লাইভ টিভি",
|
"HeaderLiveTV": "লাইভ টিভি",
|
||||||
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
|
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
|
||||||
"HeaderFavoriteShows": "প্রিয় শোগুলো"
|
"HeaderFavoriteShows": "প্রিয় শোগুলো",
|
||||||
|
"TasksLibraryCategory": "গ্রন্থাগার",
|
||||||
|
"TasksMaintenanceCategory": "রক্ষণাবেক্ষণ"
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,5 +99,13 @@
|
||||||
"TaskCleanCache": "נקה תיקיית מטמון",
|
"TaskCleanCache": "נקה תיקיית מטמון",
|
||||||
"TasksApplicationCategory": "יישום",
|
"TasksApplicationCategory": "יישום",
|
||||||
"TasksLibraryCategory": "ספרייה",
|
"TasksLibraryCategory": "ספרייה",
|
||||||
"TasksMaintenanceCategory": "תחזוקה"
|
"TasksMaintenanceCategory": "תחזוקה",
|
||||||
|
"TaskUpdatePlugins": "עדכן תוספים",
|
||||||
|
"TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
|
||||||
|
"TaskRefreshPeople": "רענן אנשים",
|
||||||
|
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
|
||||||
|
"TaskCleanLogs": "נקה תיקיית יומן",
|
||||||
|
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
|
||||||
|
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||||
|
"TasksChannelsCategory": "ערוצי אינטרנט"
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,5 +97,9 @@
|
||||||
"TasksApplicationCategory": "Applikasjon",
|
"TasksApplicationCategory": "Applikasjon",
|
||||||
"TasksLibraryCategory": "Bibliotek",
|
"TasksLibraryCategory": "Bibliotek",
|
||||||
"TasksMaintenanceCategory": "Vedlikehold",
|
"TasksMaintenanceCategory": "Vedlikehold",
|
||||||
"TaskCleanCache": "Tøm buffer katalog"
|
"TaskCleanCache": "Tøm buffer katalog",
|
||||||
|
"TaskRefreshLibrary": "Skann mediebibliotek",
|
||||||
|
"TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.",
|
||||||
|
"TaskRefreshChapterImages": "Trekk ut Kapittelbilder",
|
||||||
|
"TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet."
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Middleware
|
|
||||||
{
|
|
||||||
public class WebSocketMiddleware
|
|
||||||
{
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
private readonly ILogger<WebSocketMiddleware> _logger;
|
|
||||||
private readonly WebSocketManager _webSocketManager;
|
|
||||||
|
|
||||||
public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
_logger = logger;
|
|
||||||
_webSocketManager = webSocketManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Invoke(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Handling request: " + httpContext.Request.Path);
|
|
||||||
|
|
||||||
if (httpContext.WebSockets.IsWebSocketRequest)
|
|
||||||
{
|
|
||||||
var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
|
|
||||||
if (webSocketContext != null)
|
|
||||||
{
|
|
||||||
await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _next.Invoke(httpContext).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Net
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interface IWebSocket
|
|
||||||
/// </summary>
|
|
||||||
public interface IWebSocket : IDisposable
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [closed].
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<EventArgs> Closed;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the state.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The state.</value>
|
|
||||||
WebSocketState State { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the receive action.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The receive action.</value>
|
|
||||||
Action<byte[]> OnReceiveBytes { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the async.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bytes">The bytes.</param>
|
|
||||||
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the asynchronous.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">The text.</param>
|
|
||||||
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
using System;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Net
|
|
||||||
{
|
|
||||||
public class WebSocketConnectEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the URL.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The URL.</value>
|
|
||||||
public string Url { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the query string.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The query string.</value>
|
|
||||||
public IQueryCollection QueryString { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the web socket.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The web socket.</value>
|
|
||||||
public IWebSocket WebSocket { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the endpoint.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The endpoint.</value>
|
|
||||||
public string Endpoint { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Session;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Session
|
|
||||||
{
|
|
||||||
public class HttpSessionController : ISessionController
|
|
||||||
{
|
|
||||||
private readonly IHttpClient _httpClient;
|
|
||||||
private readonly IJsonSerializer _json;
|
|
||||||
private readonly ISessionManager _sessionManager;
|
|
||||||
|
|
||||||
public SessionInfo Session { get; private set; }
|
|
||||||
|
|
||||||
private readonly string _postUrl;
|
|
||||||
|
|
||||||
public HttpSessionController(IHttpClient httpClient,
|
|
||||||
IJsonSerializer json,
|
|
||||||
SessionInfo session,
|
|
||||||
string postUrl, ISessionManager sessionManager)
|
|
||||||
{
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_json = json;
|
|
||||||
Session = session;
|
|
||||||
_postUrl = postUrl;
|
|
||||||
_sessionManager = sessionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl);
|
|
||||||
|
|
||||||
public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5;
|
|
||||||
|
|
||||||
public bool SupportsMediaControl => true;
|
|
||||||
|
|
||||||
private Task SendMessage(string name, string messageId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
args["messageId"] = messageId;
|
|
||||||
var url = PostUrl + "/" + name + ToQueryString(args);
|
|
||||||
|
|
||||||
return SendRequest(new HttpRequestOptions
|
|
||||||
{
|
|
||||||
Url = url,
|
|
||||||
CancellationToken = cancellationToken,
|
|
||||||
BufferContent = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var dict = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
|
|
||||||
|
|
||||||
if (command.StartPositionTicks.HasValue)
|
|
||||||
{
|
|
||||||
dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
if (command.AudioStreamIndex.HasValue)
|
|
||||||
{
|
|
||||||
dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
if (command.SubtitleStreamIndex.HasValue)
|
|
||||||
{
|
|
||||||
dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
if (command.StartIndex.HasValue)
|
|
||||||
{
|
|
||||||
dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrEmpty(command.MediaSourceId))
|
|
||||||
{
|
|
||||||
dict["MediaSourceId"] = command.MediaSourceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var args = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
if (command.Command == PlaystateCommand.Seek)
|
|
||||||
{
|
|
||||||
if (!command.SeekPositionTicks.HasValue)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("SeekPositionTicks cannot be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SendMessage(command.Command.ToString(), messageId, args, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string[] _supportedMessages = Array.Empty<string>();
|
|
||||||
public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!IsSessionActive)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return SendPlayCommand(data as PlayRequest, messageId, cancellationToken);
|
|
||||||
}
|
|
||||||
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken);
|
|
||||||
}
|
|
||||||
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var command = data as GeneralCommand;
|
|
||||||
return SendMessage(command.Name, messageId, command.Arguments, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = PostUrl + "/" + name;
|
|
||||||
|
|
||||||
url += "?messageId=" + messageId;
|
|
||||||
|
|
||||||
var options = new HttpRequestOptions
|
|
||||||
{
|
|
||||||
Url = url,
|
|
||||||
CancellationToken = cancellationToken,
|
|
||||||
BufferContent = false
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data != null)
|
|
||||||
{
|
|
||||||
if (typeof(T) == typeof(string))
|
|
||||||
{
|
|
||||||
var str = data as string;
|
|
||||||
if (!string.IsNullOrEmpty(str))
|
|
||||||
{
|
|
||||||
options.RequestContent = str;
|
|
||||||
options.RequestContentType = "application/json";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
options.RequestContent = _json.SerializeToString(data);
|
|
||||||
options.RequestContentType = "application/json";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SendRequest(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendRequest(HttpRequestOptions options)
|
|
||||||
{
|
|
||||||
using (var response = await _httpClient.Post(options).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ToQueryString(Dictionary<string, string> nvc)
|
|
||||||
{
|
|
||||||
var array = (from item in nvc
|
|
||||||
select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var args = string.Join("&", array);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(args))
|
|
||||||
{
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "?" + args;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -490,8 +490,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
Client = appName,
|
Client = appName,
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
ApplicationVersion = appVersion,
|
ApplicationVersion = appVersion,
|
||||||
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
|
||||||
ServerId = _appHost.SystemId
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var username = user?.Username;
|
var username = user?.Username;
|
||||||
|
@ -1052,12 +1051,12 @@ namespace Emby.Server.Implementations.Session
|
||||||
|
|
||||||
private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
|
private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var controllers = session.SessionControllers.ToArray();
|
var controllers = session.SessionControllers;
|
||||||
var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
var messageId = Guid.NewGuid();
|
||||||
|
|
||||||
foreach (var controller in controllers)
|
foreach (var controller in controllers)
|
||||||
{
|
{
|
||||||
await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false);
|
await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1065,13 +1064,13 @@ namespace Emby.Server.Implementations.Session
|
||||||
{
|
{
|
||||||
IEnumerable<Task> GetTasks()
|
IEnumerable<Task> GetTasks()
|
||||||
{
|
{
|
||||||
var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
var messageId = Guid.NewGuid();
|
||||||
foreach (var session in sessions)
|
foreach (var session in sessions)
|
||||||
{
|
{
|
||||||
var controllers = session.SessionControllers;
|
var controllers = session.SessionControllers;
|
||||||
foreach (var controller in controllers)
|
foreach (var controller in controllers)
|
||||||
{
|
{
|
||||||
yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken);
|
yield return controller.SendMessage(name, messageId, data, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1772,7 +1771,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
throw new ArgumentNullException(nameof(info));
|
throw new ArgumentNullException(nameof(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = info.UserId.Equals(Guid.Empty)
|
var user = info.UserId == Guid.Empty
|
||||||
? null
|
? null
|
||||||
: _userManager.GetUserById(info.UserId);
|
: _userManager.GetUserById(info.UserId);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Events;
|
using MediaBrowser.Model.Events;
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
@ -12,7 +11,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class SessionWebSocketListener
|
/// Class SessionWebSocketListener
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SessionWebSocketListener : IWebSocketListener, IDisposable
|
public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _session manager
|
/// The _session manager
|
||||||
|
@ -23,42 +22,41 @@ namespace Emby.Server.Implementations.Session
|
||||||
/// The _logger
|
/// The _logger
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
/// <summary>
|
|
||||||
/// The _dto service
|
|
||||||
/// </summary>
|
|
||||||
private readonly IJsonSerializer _json;
|
|
||||||
|
|
||||||
private readonly IHttpServer _httpServer;
|
private readonly IHttpServer _httpServer;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
|
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="sessionManager">The session manager.</param>
|
/// <param name="sessionManager">The session manager.</param>
|
||||||
/// <param name="loggerFactory">The logger factory.</param>
|
/// <param name="loggerFactory">The logger factory.</param>
|
||||||
/// <param name="json">The json.</param>
|
|
||||||
/// <param name="httpServer">The HTTP server.</param>
|
/// <param name="httpServer">The HTTP server.</param>
|
||||||
public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer)
|
public SessionWebSocketListener(
|
||||||
|
ILogger<SessionWebSocketListener> logger,
|
||||||
|
ISessionManager sessionManager,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IHttpServer httpServer)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_logger = loggerFactory.CreateLogger(GetType().Name);
|
_loggerFactory = loggerFactory;
|
||||||
_json = json;
|
|
||||||
_httpServer = httpServer;
|
_httpServer = httpServer;
|
||||||
httpServer.WebSocketConnected += _serverManager_WebSocketConnected;
|
|
||||||
|
httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
|
private void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
|
||||||
{
|
{
|
||||||
var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint);
|
var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
|
||||||
|
|
||||||
if (session != null)
|
if (session != null)
|
||||||
{
|
{
|
||||||
EnsureController(session, e.Argument);
|
EnsureController(session, e.Argument);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url);
|
_logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,9 +77,10 @@ namespace Emby.Server.Implementations.Session
|
||||||
return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
|
return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_httpServer.WebSocketConnected -= _serverManager_WebSocketConnected;
|
_httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -94,7 +93,8 @@ namespace Emby.Server.Implementations.Session
|
||||||
|
|
||||||
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
|
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
|
||||||
{
|
{
|
||||||
var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager));
|
var controllerInfo = session.EnsureController<WebSocketController>(
|
||||||
|
s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
|
||||||
|
|
||||||
var controller = (WebSocketController)controllerInfo.Item1;
|
var controller = (WebSocketController)controllerInfo.Item1;
|
||||||
controller.AddWebSocket(connection);
|
controller.AddWebSocket(connection);
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
#pragma warning disable CS1591
|
||||||
|
#pragma warning disable SA1600
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Session
|
namespace Emby.Server.Implementations.Session
|
||||||
{
|
{
|
||||||
public class WebSocketController : ISessionController, IDisposable
|
public sealed class WebSocketController : ISessionController, IDisposable
|
||||||
{
|
{
|
||||||
public SessionInfo Session { get; private set; }
|
|
||||||
public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; }
|
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
|
private readonly SessionInfo _session;
|
||||||
|
|
||||||
public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager)
|
private readonly List<IWebSocketConnection> _sockets;
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
public WebSocketController(
|
||||||
|
ILogger<WebSocketController> logger,
|
||||||
|
SessionInfo session,
|
||||||
|
ISessionManager sessionManager)
|
||||||
{
|
{
|
||||||
Session = session;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_session = session;
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
Sockets = new List<IWebSocketConnection>();
|
_sockets = new List<IWebSocketConnection>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasOpenSockets => GetActiveSockets().Any();
|
private bool HasOpenSockets => GetActiveSockets().Any();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public bool SupportsMediaControl => HasOpenSockets;
|
public bool SupportsMediaControl => HasOpenSockets;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public bool IsSessionActive => HasOpenSockets;
|
public bool IsSessionActive => HasOpenSockets;
|
||||||
|
|
||||||
private IEnumerable<IWebSocketConnection> GetActiveSockets()
|
private IEnumerable<IWebSocketConnection> GetActiveSockets()
|
||||||
{
|
=> _sockets.Where(i => i.State == WebSocketState.Open);
|
||||||
return Sockets
|
|
||||||
.OrderByDescending(i => i.LastActivityDate)
|
|
||||||
.Where(i => i.State == WebSocketState.Open);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddWebSocket(IWebSocketConnection connection)
|
public void AddWebSocket(IWebSocketConnection connection)
|
||||||
{
|
{
|
||||||
var sockets = Sockets.ToList();
|
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
|
||||||
sockets.Add(connection);
|
_sockets.Add(connection);
|
||||||
|
|
||||||
Sockets = sockets;
|
connection.Closed += OnConnectionClosed;
|
||||||
|
|
||||||
connection.Closed += connection_Closed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void connection_Closed(object sender, EventArgs e)
|
private void OnConnectionClosed(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var connection = (IWebSocketConnection)sender;
|
var connection = (IWebSocketConnection)sender;
|
||||||
var sockets = Sockets.ToList();
|
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||||
sockets.Remove(connection);
|
_sockets.Remove(connection);
|
||||||
|
connection.Closed -= OnConnectionClosed;
|
||||||
Sockets = sockets;
|
_sessionManager.CloseIfNeeded(_session);
|
||||||
|
|
||||||
_sessionManager.CloseIfNeeded(Session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
|
/// <inheritdoc />
|
||||||
|
public Task SendMessage<T>(
|
||||||
|
string name,
|
||||||
|
Guid messageId,
|
||||||
|
T data,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var socket = GetActiveSockets()
|
var socket = GetActiveSockets()
|
||||||
|
.OrderByDescending(i => i.LastActivityDate)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (socket == null)
|
if (socket == null)
|
||||||
|
@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
return socket.SendAsync(new WebSocketMessage<T>
|
return socket.SendAsync(
|
||||||
{
|
new WebSocketMessage<T>
|
||||||
Data = data,
|
{
|
||||||
MessageType = name,
|
Data = data,
|
||||||
MessageId = messageId
|
MessageType = name,
|
||||||
|
MessageId = messageId
|
||||||
}, cancellationToken);
|
},
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var socket in Sockets.ToList())
|
if (_disposed)
|
||||||
{
|
{
|
||||||
socket.Closed -= connection_Closed;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var socket in _sockets)
|
||||||
|
{
|
||||||
|
socket.Closed -= OnConnectionClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Server.Implementations.Net;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.SocketSharp
|
|
||||||
{
|
|
||||||
public class SharpWebSocket : IWebSocket
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The logger
|
|
||||||
/// </summary>
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
public event EventHandler<EventArgs> Closed;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the web socket.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The web socket.</value>
|
|
||||||
private readonly WebSocket _webSocket;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public SharpWebSocket(WebSocket socket, ILogger logger)
|
|
||||||
{
|
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
||||||
_webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the state.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The state.</value>
|
|
||||||
public WebSocketState State => _webSocket.State;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the async.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bytes">The bytes.</param>
|
|
||||||
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the asynchronous.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">The text.</param>
|
|
||||||
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool dispose)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dispose)
|
|
||||||
{
|
|
||||||
_cancellationTokenSource.Cancel();
|
|
||||||
if (_webSocket.State == WebSocketState.Open)
|
|
||||||
{
|
|
||||||
_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
|
|
||||||
CancellationToken.None);
|
|
||||||
}
|
|
||||||
Closed?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the receive action.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The receive action.</value>
|
|
||||||
public Action<byte[]> OnReceiveBytes { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Server.Implementations.HttpServer;
|
|
||||||
using Emby.Server.Implementations.Net;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.SocketSharp
|
|
||||||
{
|
|
||||||
public class WebSocketSharpListener : IHttpListener
|
|
||||||
{
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
|
||||||
private CancellationToken _disposeCancellationToken;
|
|
||||||
|
|
||||||
public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_disposeCancellationToken = _disposeCancellationTokenSource.Token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
|
|
||||||
|
|
||||||
public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
|
|
||||||
|
|
||||||
public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
|
|
||||||
|
|
||||||
private static void LogRequest(ILogger logger, HttpRequest request)
|
|
||||||
{
|
|
||||||
var url = request.GetDisplayUrl();
|
|
||||||
|
|
||||||
logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ProcessWebSocketRequest(HttpContext ctx)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LogRequest(_logger, ctx.Request);
|
|
||||||
var endpoint = ctx.Connection.RemoteIpAddress.ToString();
|
|
||||||
var url = ctx.Request.GetDisplayUrl();
|
|
||||||
|
|
||||||
var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
|
|
||||||
var socket = new SharpWebSocket(webSocketContext, _logger);
|
|
||||||
|
|
||||||
WebSocketConnected(new WebSocketConnectEventArgs
|
|
||||||
{
|
|
||||||
Url = url,
|
|
||||||
QueryString = ctx.Request.Query,
|
|
||||||
WebSocket = socket,
|
|
||||||
Endpoint = endpoint
|
|
||||||
});
|
|
||||||
|
|
||||||
WebSocketReceiveResult result;
|
|
||||||
var message = new List<byte>();
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var buffer = WebSocket.CreateServerBuffer(4096);
|
|
||||||
result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
|
|
||||||
message.AddRange(buffer.Array.Take(result.Count));
|
|
||||||
|
|
||||||
if (result.EndOfMessage)
|
|
||||||
{
|
|
||||||
socket.OnReceiveBytes(message.ToArray());
|
|
||||||
message.Clear();
|
|
||||||
}
|
|
||||||
} while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
|
|
||||||
|
|
||||||
|
|
||||||
if (webSocketContext.State == WebSocketState.Open)
|
|
||||||
{
|
|
||||||
await webSocketContext.CloseAsync(
|
|
||||||
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
|
|
||||||
result.CloseStatusDescription,
|
|
||||||
_disposeCancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "AcceptWebSocketAsync error");
|
|
||||||
if (!ctx.Response.HasStarted)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Stop()
|
|
||||||
{
|
|
||||||
_disposeCancellationTokenSource.Cancel();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases the unmanaged resources and disposes of the managed resources used.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases the unmanaged resources and disposes of the managed resources used.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing">Whether or not the managed resources should be disposed.</param>
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
Stop().GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.WebSockets
|
|
||||||
{
|
|
||||||
public interface IWebSocketHandler
|
|
||||||
{
|
|
||||||
Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using UtfUnknown;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.WebSockets
|
|
||||||
{
|
|
||||||
public class WebSocketManager
|
|
||||||
{
|
|
||||||
private readonly IWebSocketHandler[] _webSocketHandlers;
|
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
|
||||||
private readonly ILogger<WebSocketManager> _logger;
|
|
||||||
private const int BufferSize = 4096;
|
|
||||||
|
|
||||||
public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
|
|
||||||
{
|
|
||||||
_webSocketHandlers = webSocketHandlers;
|
|
||||||
_jsonSerializer = jsonSerializer;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnWebSocketConnected(WebSocket webSocket)
|
|
||||||
{
|
|
||||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
|
||||||
var cancellationToken = new CancellationTokenSource().Token;
|
|
||||||
WebSocketReceiveResult result;
|
|
||||||
var message = new List<byte>();
|
|
||||||
|
|
||||||
// Keep listening for incoming messages, otherwise the socket closes automatically
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var buffer = WebSocket.CreateServerBuffer(BufferSize);
|
|
||||||
result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
|
|
||||||
message.AddRange(buffer.Array.Take(result.Count));
|
|
||||||
|
|
||||||
if (result.EndOfMessage)
|
|
||||||
{
|
|
||||||
await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false);
|
|
||||||
message.Clear();
|
|
||||||
}
|
|
||||||
} while (!taskCompletionSource.Task.IsCompleted &&
|
|
||||||
webSocket.State == WebSocketState.Open &&
|
|
||||||
result.MessageType != WebSocketMessageType.Close);
|
|
||||||
|
|
||||||
if (webSocket.State == WebSocketState.Open)
|
|
||||||
{
|
|
||||||
await webSocket.CloseAsync(
|
|
||||||
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
|
|
||||||
result.CloseStatusDescription,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
|
|
||||||
{
|
|
||||||
var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
|
|
||||||
var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
|
|
||||||
: Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
|
|
||||||
|
|
||||||
// All messages are expected to be valid JSON objects
|
|
||||||
if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
|
|
||||||
|
|
||||||
_logger.LogDebug("Websocket message received: {0}", info.MessageType);
|
|
||||||
|
|
||||||
var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
|
|
||||||
handler.GetType().Name, info.MessageType ?? string.Empty);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error processing web socket message");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +1,45 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Jellyfin.Data.Entities
|
namespace Jellyfin.Data.Entities
|
||||||
{
|
{
|
||||||
[Table("ActivityLog")]
|
/// <summary>
|
||||||
public partial class ActivityLog
|
/// An entity referencing an activity log entry.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ActivityLog : ISavingChanges
|
||||||
{
|
{
|
||||||
partial void Init();
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ActivityLog"/> class.
|
||||||
|
/// Public constructor with required data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name.</param>
|
||||||
|
/// <param name="type">The type.</param>
|
||||||
|
/// <param name="userId">The user id.</param>
|
||||||
|
public ActivityLog(string name, string type, Guid userId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(type))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Name = name;
|
||||||
|
this.Type = type;
|
||||||
|
this.UserId = userId;
|
||||||
|
this.DateCreated = DateTime.UtcNow;
|
||||||
|
this.LogSeverity = LogLevel.Trace;
|
||||||
|
|
||||||
|
Init();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ActivityLog"/> class.
|
||||||
/// Default constructor. Protected due to required properties, but present because EF needs it.
|
/// Default constructor. Protected due to required properties, but present because EF needs it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected ActivityLog()
|
protected ActivityLog()
|
||||||
|
@ -22,51 +47,16 @@ namespace Jellyfin.Data.Entities
|
||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
|
|
||||||
/// </summary>
|
|
||||||
public static ActivityLog CreateActivityLogUnsafe()
|
|
||||||
{
|
|
||||||
return new ActivityLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Public constructor with required data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name"></param>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <param name="userid"></param>
|
|
||||||
/// <param name="datecreated"></param>
|
|
||||||
/// <param name="logseverity"></param>
|
|
||||||
public ActivityLog(string name, string type, Guid userid, DateTime datecreated, Microsoft.Extensions.Logging.LogLevel logseverity)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
|
|
||||||
this.Name = name;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(type)) throw new ArgumentNullException(nameof(type));
|
|
||||||
this.Type = type;
|
|
||||||
|
|
||||||
this.UserId = userid;
|
|
||||||
|
|
||||||
this.DateCreated = datecreated;
|
|
||||||
|
|
||||||
this.LogSeverity = logseverity;
|
|
||||||
|
|
||||||
|
|
||||||
Init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Static create function (for use in LINQ queries, etc.)
|
/// Static create function (for use in LINQ queries, etc.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name"></param>
|
/// <param name="name">The name.</param>
|
||||||
/// <param name="type"></param>
|
/// <param name="type">The type.</param>
|
||||||
/// <param name="userid"></param>
|
/// <param name="userId">The user's id.</param>
|
||||||
/// <param name="datecreated"></param>
|
/// <returns>The new <see cref="ActivityLog"/> instance.</returns>
|
||||||
/// <param name="logseverity"></param>
|
public static ActivityLog Create(string name, string type, Guid userId)
|
||||||
public static ActivityLog Create(string name, string type, Guid userid, DateTime datecreated, Microsoft.Extensions.Logging.LogLevel logseverity)
|
|
||||||
{
|
{
|
||||||
return new ActivityLog(name, type, userid, datecreated, logseverity);
|
return new ActivityLog(name, type, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*************************************************************************
|
/*************************************************************************
|
||||||
|
@ -74,7 +64,8 @@ namespace Jellyfin.Data.Entities
|
||||||
*************************************************************************/
|
*************************************************************************/
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Identity, Indexed, Required
|
/// Gets or sets the identity of this instance.
|
||||||
|
/// This is the key in the backing database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key]
|
[Key]
|
||||||
[Required]
|
[Required]
|
||||||
|
@ -82,7 +73,8 @@ namespace Jellyfin.Data.Entities
|
||||||
public int Id { get; protected set; }
|
public int Id { get; protected set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required, Max length = 512
|
/// Gets or sets the name.
|
||||||
|
/// Required, Max length = 512.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(512)]
|
[MaxLength(512)]
|
||||||
|
@ -90,21 +82,24 @@ namespace Jellyfin.Data.Entities
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max length = 512
|
/// Gets or sets the overview.
|
||||||
|
/// Max length = 512.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MaxLength(512)]
|
[MaxLength(512)]
|
||||||
[StringLength(512)]
|
[StringLength(512)]
|
||||||
public string Overview { get; set; }
|
public string Overview { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max length = 512
|
/// Gets or sets the short overview.
|
||||||
|
/// Max length = 512.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MaxLength(512)]
|
[MaxLength(512)]
|
||||||
[StringLength(512)]
|
[StringLength(512)]
|
||||||
public string ShortOverview { get; set; }
|
public string ShortOverview { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required, Max length = 256
|
/// Gets or sets the type.
|
||||||
|
/// Required, Max length = 256.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(256)]
|
[MaxLength(256)]
|
||||||
|
@ -112,42 +107,48 @@ namespace Jellyfin.Data.Entities
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required
|
/// Gets or sets the user id.
|
||||||
|
/// Required.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max length = 256
|
/// Gets or sets the item id.
|
||||||
|
/// Max length = 256.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MaxLength(256)]
|
[MaxLength(256)]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
public string ItemId { get; set; }
|
public string ItemId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required
|
/// Gets or sets the date created. This should be in UTC.
|
||||||
|
/// Required.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public DateTime DateCreated { get; set; }
|
public DateTime DateCreated { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required
|
/// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>.
|
||||||
|
/// Required.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public Microsoft.Extensions.Logging.LogLevel LogSeverity { get; set; }
|
public LogLevel LogSeverity { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required, ConcurrenyToken
|
/// Gets or sets the row version.
|
||||||
|
/// Required, ConcurrencyToken.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ConcurrencyCheck]
|
[ConcurrencyCheck]
|
||||||
[Required]
|
[Required]
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; set; }
|
||||||
|
|
||||||
|
partial void Init();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.3" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
@ -14,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Activity
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActivityManager : IActivityManager
|
public class ActivityManager : IActivityManager
|
||||||
{
|
{
|
||||||
private JellyfinDbProvider _provider;
|
private readonly JellyfinDbProvider _provider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
|
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
|
||||||
|
@ -50,31 +49,31 @@ namespace Jellyfin.Server.Implementations.Activity
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public QueryResult<ActivityLogEntry> GetPagedResult(
|
public QueryResult<ActivityLogEntry> GetPagedResult(
|
||||||
Func<IQueryable<ActivityLog>, IEnumerable<ActivityLog>> func,
|
Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
|
||||||
int? startIndex,
|
int? startIndex,
|
||||||
int? limit)
|
int? limit)
|
||||||
{
|
{
|
||||||
using var dbContext = _provider.CreateContext();
|
using var dbContext = _provider.CreateContext();
|
||||||
|
|
||||||
var result = func.Invoke(dbContext.ActivityLogs).AsQueryable();
|
var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated));
|
||||||
|
|
||||||
if (startIndex.HasValue)
|
if (startIndex.HasValue)
|
||||||
{
|
{
|
||||||
result = result.Where(entry => entry.Id >= startIndex.Value);
|
query = query.Skip(startIndex.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (limit.HasValue)
|
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.
|
// 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.Select(ConvertToOldModel).ToList();
|
||||||
|
|
||||||
return new QueryResult<ActivityLogEntry>()
|
return new QueryResult<ActivityLogEntry>
|
||||||
{
|
{
|
||||||
Items = list,
|
Items = list,
|
||||||
TotalRecordCount = list.Count
|
TotalRecordCount = func(dbContext.ActivityLogs).Count()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,14 @@
|
||||||
<Compile Remove="Migrations\20200504195424_UserSchema.Designer.cs" />
|
<Compile Remove="Migrations\20200504195424_UserSchema.Designer.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
|
<ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
|
|
|
@ -107,9 +107,10 @@ namespace Jellyfin.Server.Implementations
|
||||||
|
|
||||||
public override int SaveChanges()
|
public override int SaveChanges()
|
||||||
{
|
{
|
||||||
foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified))
|
foreach (var saveEntity in ChangeTracker.Entries()
|
||||||
|
.Where(e => e.State == EntityState.Modified)
|
||||||
|
.OfType<ISavingChanges>())
|
||||||
{
|
{
|
||||||
var saveEntity = entity.Entity as ISavingChanges;
|
|
||||||
saveEntity.OnSavingChanges();
|
saveEntity.OnSavingChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations
|
||||||
/// <returns>The newly created context.</returns>
|
/// <returns>The newly created context.</returns>
|
||||||
public JellyfinDb CreateContext()
|
public JellyfinDb CreateContext()
|
||||||
{
|
{
|
||||||
return _serviceProvider.GetService<JellyfinDb>();
|
return _serviceProvider.GetRequiredService<JellyfinDb>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
#pragma warning disable SA1601
|
|
||||||
|
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
|
@ -12,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
namespace Jellyfin.Server.Implementations.Migrations
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(JellyfinDb))]
|
[DbContext(typeof(JellyfinDb))]
|
||||||
[Migration("20200502231229_InitialSchema")]
|
[Migration("20200514181226_AddActivityLog")]
|
||||||
partial class InitialSchema
|
partial class AddActivityLog
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
@ -65,7 +64,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ActivityLog");
|
b.ToTable("ActivityLogs");
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
#pragma warning disable SA1601
|
#pragma warning disable SA1601
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
namespace Jellyfin.Server.Implementations.Migrations
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
{
|
{
|
||||||
public partial class InitialSchema : Migration
|
public partial class AddActivityLog : Migration
|
||||||
{
|
{
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
name: "jellyfin");
|
name: "jellyfin");
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "ActivityLog",
|
name: "ActivityLogs",
|
||||||
schema: "jellyfin",
|
schema: "jellyfin",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
|
@ -32,14 +32,14 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_ActivityLog", x => x.Id);
|
table.PrimaryKey("PK_ActivityLogs", x => x.Id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "ActivityLog",
|
name: "ActivityLogs",
|
||||||
schema: "jellyfin");
|
schema: "jellyfin");
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,3 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
#pragma warning disable SA1601
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Jellyfin.Server.Implementations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Implementations.Migrations
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
{
|
{
|
||||||
|
@ -60,7 +58,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ActivityLog");
|
b.ToTable("ActivityLogs");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Group", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Group", b =>
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Emby.Drawing;
|
using Emby.Drawing;
|
||||||
using Emby.Server.Implementations;
|
using Emby.Server.Implementations;
|
||||||
using Jellyfin.Drawing.Skia;
|
using Jellyfin.Drawing.Skia;
|
||||||
|
using Jellyfin.Server.Implementations;
|
||||||
|
using Jellyfin.Server.Implementations.Activity;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
using MediaBrowser.Model.Activity;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
@ -56,6 +61,15 @@ namespace Jellyfin.Server
|
||||||
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
|
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Set up scoping and use AddDbContextPool
|
||||||
|
serviceCollection.AddDbContext<JellyfinDb>(
|
||||||
|
options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
|
||||||
|
ServiceLifetime.Transient);
|
||||||
|
|
||||||
|
serviceCollection.AddSingleton<JellyfinDbProvider>();
|
||||||
|
|
||||||
|
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
|
||||||
|
|
||||||
base.RegisterServices(serviceCollection);
|
base.RegisterServices(serviceCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,6 @@
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
<!-- Used for generating migrations for EF Core -->
|
|
||||||
<GenerateRuntimeConfigurationFiles>True</GenerateRuntimeConfigurationFiles>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -45,10 +42,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.7.82" />
|
<PackageReference Include="CommandLineParser" Version="2.7.82" />
|
||||||
<PackageReference Include="Json.Net" Version="1.0.22" />
|
<PackageReference Include="Json.Net" Version="1.0.22" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
|
||||||
<PackageReference Include="prometheus-net" Version="3.5.0" />
|
<PackageReference Include="prometheus-net" Version="3.5.0" />
|
||||||
|
@ -68,6 +61,7 @@
|
||||||
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
|
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
|
||||||
<ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
|
<ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
|
||||||
<ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
|
<ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
|
||||||
|
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -19,6 +19,7 @@ namespace Jellyfin.Server.Migrations
|
||||||
typeof(Routines.DisableTranscodingThrottling),
|
typeof(Routines.DisableTranscodingThrottling),
|
||||||
typeof(Routines.CreateUserLoggingConfigFile),
|
typeof(Routines.CreateUserLoggingConfigFile),
|
||||||
typeof(Routines.MigrateActivityLogDb),
|
typeof(Routines.MigrateActivityLogDb),
|
||||||
|
typeof(Routines.RemoveDuplicateExtras),
|
||||||
typeof(Routines.MigrateUserDb)
|
typeof(Routines.MigrateUserDb)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Emby.Server.Implementations.Data;
|
using Emby.Server.Implementations.Data;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations;
|
||||||
|
@ -12,6 +12,9 @@ using SQLitePCL.pretty;
|
||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Routines
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The migration routine for migrating the activity log database to EF Core.
|
||||||
|
/// </summary>
|
||||||
public class MigrateActivityLogDb : IMigrationRoutine
|
public class MigrateActivityLogDb : IMigrationRoutine
|
||||||
{
|
{
|
||||||
private const string DbFilename = "activitylog.db";
|
private const string DbFilename = "activitylog.db";
|
||||||
|
@ -20,6 +23,12 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||||
private readonly JellyfinDbProvider _provider;
|
private readonly JellyfinDbProvider _provider;
|
||||||
private readonly IServerApplicationPaths _paths;
|
private readonly IServerApplicationPaths _paths;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MigrateActivityLogDb"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="paths">The server application paths.</param>
|
||||||
|
/// <param name="provider">The database provider.</param>
|
||||||
public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
|
public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
@ -27,19 +36,35 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||||
_paths = paths;
|
_paths = paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978");
|
public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978");
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
public string Name => "MigrateActivityLogDatabase";
|
public string Name => "MigrateActivityLogDatabase";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
public void Perform()
|
public void Perform()
|
||||||
{
|
{
|
||||||
|
var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "None", LogLevel.None },
|
||||||
|
{ "Trace", LogLevel.Trace },
|
||||||
|
{ "Debug", LogLevel.Debug },
|
||||||
|
{ "Information", LogLevel.Information },
|
||||||
|
{ "Info", LogLevel.Information },
|
||||||
|
{ "Warn", LogLevel.Warning },
|
||||||
|
{ "Warning", LogLevel.Warning },
|
||||||
|
{ "Error", LogLevel.Error },
|
||||||
|
{ "Critical", LogLevel.Critical }
|
||||||
|
};
|
||||||
|
|
||||||
var dataPath = _paths.DataPath;
|
var dataPath = _paths.DataPath;
|
||||||
using (var connection = SQLite3.Open(
|
using (var connection = SQLite3.Open(
|
||||||
Path.Combine(dataPath, DbFilename),
|
Path.Combine(dataPath, DbFilename),
|
||||||
ConnectionFlags.ReadOnly,
|
ConnectionFlags.ReadOnly,
|
||||||
null))
|
null))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Migrating the database may take a while, do not stop Jellyfin.");
|
_logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
|
||||||
using var dbContext = _provider.CreateContext();
|
using var dbContext = _provider.CreateContext();
|
||||||
|
|
||||||
var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC");
|
var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC");
|
||||||
|
@ -51,14 +76,21 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||||
dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';");
|
dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';");
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
|
|
||||||
foreach (var entry in queryResult)
|
var newEntries = queryResult.Select(entry =>
|
||||||
{
|
{
|
||||||
|
if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity))
|
||||||
|
{
|
||||||
|
severity = LogLevel.Trace;
|
||||||
|
}
|
||||||
|
|
||||||
var newEntry = new ActivityLog(
|
var newEntry = new ActivityLog(
|
||||||
entry[1].ToString(),
|
entry[1].ToString(),
|
||||||
entry[4].ToString(),
|
entry[4].ToString(),
|
||||||
entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString()),
|
entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString()))
|
||||||
entry[7].ReadDateTime(),
|
{
|
||||||
ParseLogLevel(entry[8].ToString()));
|
DateCreated = entry[7].ReadDateTime(),
|
||||||
|
LogSeverity = severity
|
||||||
|
};
|
||||||
|
|
||||||
if (entry[2].SQLiteType != SQLiteType.Null)
|
if (entry[2].SQLiteType != SQLiteType.Null)
|
||||||
{
|
{
|
||||||
|
@ -75,46 +107,27 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||||
newEntry.ItemId = entry[5].ToString();
|
newEntry.ItemId = entry[5].ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.ActivityLogs.Add(newEntry);
|
return newEntry;
|
||||||
dbContext.SaveChanges();
|
});
|
||||||
}
|
|
||||||
|
dbContext.ActivityLogs.AddRange(newEntries);
|
||||||
|
dbContext.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
|
File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
|
||||||
|
|
||||||
|
var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
|
||||||
|
if (File.Exists(journalPath))
|
||||||
|
{
|
||||||
|
File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (IOException e)
|
catch (IOException e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'");
|
_logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private LogLevel ParseLogLevel(string entry)
|
|
||||||
{
|
|
||||||
if (string.Equals(entry, "Debug", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return LogLevel.Debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(entry, "Information", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(entry, "Info", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return LogLevel.Information;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(entry, "Warning", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(entry, "Warn", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return LogLevel.Warning;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(entry, "Error", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return LogLevel.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return LogLevel.Trace;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
79
Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
Normal file
79
Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
using MediaBrowser.Controller;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SQLitePCL.pretty;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
|
||||||
|
/// </summary>
|
||||||
|
internal class RemoveDuplicateExtras : IMigrationRoutine
|
||||||
|
{
|
||||||
|
private const string DbFilename = "library.db";
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IServerApplicationPaths _paths;
|
||||||
|
|
||||||
|
public RemoveDuplicateExtras(ILogger<RemoveDuplicateExtras> logger, IServerApplicationPaths paths)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_paths = paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}");
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Name => "RemoveDuplicateExtras";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Perform()
|
||||||
|
{
|
||||||
|
var dataPath = _paths.DataPath;
|
||||||
|
var dbPath = Path.Combine(dataPath, DbFilename);
|
||||||
|
using (var connection = SQLite3.Open(
|
||||||
|
dbPath,
|
||||||
|
ConnectionFlags.ReadWrite,
|
||||||
|
null))
|
||||||
|
{
|
||||||
|
// Query the database for the ids of duplicate extras
|
||||||
|
var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
|
||||||
|
var bads = string.Join(", ", queryResult.SelectScalarString());
|
||||||
|
|
||||||
|
// Do nothing if no duplicate extras were detected
|
||||||
|
if (bads.Length == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No duplicate extras detected, skipping migration.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back up the database before deleting any entries
|
||||||
|
for (int i = 1; ; i++)
|
||||||
|
{
|
||||||
|
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
|
||||||
|
if (!File.Exists(bakPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(dbPath, bakPath);
|
||||||
|
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all duplicate extras
|
||||||
|
_logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
|
||||||
|
connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,14 +10,11 @@ using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommandLine;
|
using CommandLine;
|
||||||
using Emby.Drawing;
|
|
||||||
using Emby.Server.Implementations;
|
using Emby.Server.Implementations;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
using Emby.Server.Implementations.IO;
|
using Emby.Server.Implementations.IO;
|
||||||
using Emby.Server.Implementations.Networking;
|
using Emby.Server.Implementations.Networking;
|
||||||
using Jellyfin.Drawing.Skia;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
|
||||||
using MediaBrowser.Controller.Extensions;
|
using MediaBrowser.Controller.Extensions;
|
||||||
using MediaBrowser.WebDashboard.Api;
|
using MediaBrowser.WebDashboard.Api;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
@ -297,7 +294,7 @@ namespace Jellyfin.Server
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Kestrel listening on {IpAddress}", address);
|
_logger.LogInformation("Kestrel listening on {IpAddress}", address);
|
||||||
options.Listen(address, appHost.HttpPort);
|
options.Listen(address, appHost.HttpPort);
|
||||||
if (appHost.EnableHttps && appHost.Certificate != null)
|
if (appHost.ListenWithHttps)
|
||||||
{
|
{
|
||||||
options.Listen(address, appHost.HttpsPort, listenOptions =>
|
options.Listen(address, appHost.HttpsPort, listenOptions =>
|
||||||
{
|
{
|
||||||
|
@ -327,7 +324,7 @@ namespace Jellyfin.Server
|
||||||
_logger.LogInformation("Kestrel listening on all interfaces");
|
_logger.LogInformation("Kestrel listening on all interfaces");
|
||||||
options.ListenAnyIP(appHost.HttpPort);
|
options.ListenAnyIP(appHost.HttpPort);
|
||||||
|
|
||||||
if (appHost.EnableHttps && appHost.Certificate != null)
|
if (appHost.ListenWithHttps)
|
||||||
{
|
{
|
||||||
options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
|
options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -64,7 +64,6 @@ namespace Jellyfin.Server
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
// TODO app.UseMiddleware<WebSocketMiddleware>();
|
// TODO app.UseMiddleware<WebSocketMiddleware>();
|
||||||
app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
|
|
||||||
|
|
||||||
// TODO use when old API is removed: app.UseAuthentication();
|
// TODO use when old API is removed: app.UseAuthentication();
|
||||||
app.UseJellyfinApiSwagger();
|
app.UseJellyfinApiSwagger();
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace MediaBrowser.Api
|
||||||
public abstract class BaseApiService : IService, IRequiresRequest
|
public abstract class BaseApiService : IService, IRequiresRequest
|
||||||
{
|
{
|
||||||
public BaseApiService(
|
public BaseApiService(
|
||||||
ILogger logger,
|
ILogger<BaseApiService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory)
|
IHttpResultFactory httpResultFactory)
|
||||||
{
|
{
|
||||||
|
@ -35,7 +35,7 @@ namespace MediaBrowser.Api
|
||||||
/// Gets the logger.
|
/// Gets the logger.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The logger.</value>
|
/// <value>The logger.</value>
|
||||||
protected ILogger Logger { get; }
|
protected ILogger<BaseApiService> Logger { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the server configuration manager.
|
/// Gets or sets the server configuration manager.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
|
@ -116,11 +115,6 @@ namespace MediaBrowser.Api.Devices
|
||||||
return _deviceManager.GetDeviceOptions(request.Id);
|
return _deviceManager.GetDeviceOptions(request.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public object Get(GetCameraUploads request)
|
|
||||||
{
|
|
||||||
return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(DeleteDevice request)
|
public void Delete(DeleteDevice request)
|
||||||
{
|
{
|
||||||
var sessions = _authRepo.Get(new AuthenticationInfoQuery
|
var sessions = _authRepo.Get(new AuthenticationInfoQuery
|
||||||
|
@ -134,35 +128,5 @@ namespace MediaBrowser.Api.Devices
|
||||||
_sessionManager.Logout(session);
|
_sessionManager.Logout(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Post(PostCameraUpload request)
|
|
||||||
{
|
|
||||||
var deviceId = Request.QueryString["DeviceId"];
|
|
||||||
var album = Request.QueryString["Album"];
|
|
||||||
var id = Request.QueryString["Id"];
|
|
||||||
var name = Request.QueryString["Name"];
|
|
||||||
var req = Request.Response.HttpContext.Request;
|
|
||||||
|
|
||||||
if (req.HasFormContentType)
|
|
||||||
{
|
|
||||||
var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0];
|
|
||||||
|
|
||||||
return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo
|
|
||||||
{
|
|
||||||
MimeType = file.ContentType,
|
|
||||||
Album = album,
|
|
||||||
Name = name,
|
|
||||||
Id = id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo
|
|
||||||
{
|
|
||||||
MimeType = Request.ContentType,
|
|
||||||
Album = album,
|
|
||||||
Name = name,
|
|
||||||
Id = id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -319,11 +319,14 @@ namespace MediaBrowser.Api.Library
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly ILibraryMonitor _libraryMonitor;
|
private readonly ILibraryMonitor _libraryMonitor;
|
||||||
|
|
||||||
|
private readonly ILogger<MoviesService> _moviesServiceLogger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="LibraryService" /> class.
|
/// Initializes a new instance of the <see cref="LibraryService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LibraryService(
|
public LibraryService(
|
||||||
ILogger<LibraryService> logger,
|
ILogger<LibraryService> logger,
|
||||||
|
ILogger<MoviesService> moviesServiceLogger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IProviderManager providerManager,
|
IProviderManager providerManager,
|
||||||
|
@ -344,6 +347,7 @@ namespace MediaBrowser.Api.Library
|
||||||
_activityManager = activityManager;
|
_activityManager = activityManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
_libraryMonitor = libraryMonitor;
|
_libraryMonitor = libraryMonitor;
|
||||||
|
_moviesServiceLogger = moviesServiceLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string[] GetRepresentativeItemTypes(string contentType)
|
private string[] GetRepresentativeItemTypes(string contentType)
|
||||||
|
@ -543,7 +547,7 @@ namespace MediaBrowser.Api.Library
|
||||||
if (item is Movie || (program != null && program.IsMovie) || item is Trailer)
|
if (item is Movie || (program != null && program.IsMovie) || item is Trailer)
|
||||||
{
|
{
|
||||||
return new MoviesService(
|
return new MoviesService(
|
||||||
Logger,
|
_moviesServiceLogger,
|
||||||
ServerConfigurationManager,
|
ServerConfigurationManager,
|
||||||
ResultFactory,
|
ResultFactory,
|
||||||
_userManager,
|
_userManager,
|
||||||
|
@ -762,9 +766,7 @@ namespace MediaBrowser.Api.Library
|
||||||
_activityManager.Create(new Jellyfin.Data.Entities.ActivityLog(
|
_activityManager.Create(new Jellyfin.Data.Entities.ActivityLog(
|
||||||
string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
|
string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
|
||||||
"UserDownloadingContent",
|
"UserDownloadingContent",
|
||||||
auth.UserId,
|
auth.UserId)
|
||||||
DateTime.UtcNow,
|
|
||||||
LogLevel.Trace)
|
|
||||||
{
|
{
|
||||||
ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
|
ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
|
||||||
});
|
});
|
||||||
|
|
|
@ -82,7 +82,7 @@ namespace MediaBrowser.Api.Movies
|
||||||
/// Initializes a new instance of the <see cref="MoviesService" /> class.
|
/// Initializes a new instance of the <see cref="MoviesService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MoviesService(
|
public MoviesService(
|
||||||
ILogger logger,
|
ILogger<MoviesService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
|
|
@ -33,13 +33,18 @@ namespace MediaBrowser.Api.Movies
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The logger for the created <see cref="ItemsService"/> instances.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<ItemsService> _logger;
|
||||||
|
|
||||||
private readonly IDtoService _dtoService;
|
private readonly IDtoService _dtoService;
|
||||||
private readonly ILocalizationManager _localizationManager;
|
private readonly ILocalizationManager _localizationManager;
|
||||||
private readonly IJsonSerializer _json;
|
private readonly IJsonSerializer _json;
|
||||||
private readonly IAuthorizationContext _authContext;
|
private readonly IAuthorizationContext _authContext;
|
||||||
|
|
||||||
public TrailersService(
|
public TrailersService(
|
||||||
ILogger<TrailersService> logger,
|
ILoggerFactory loggerFactory,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
@ -48,7 +53,7 @@ namespace MediaBrowser.Api.Movies
|
||||||
ILocalizationManager localizationManager,
|
ILocalizationManager localizationManager,
|
||||||
IJsonSerializer json,
|
IJsonSerializer json,
|
||||||
IAuthorizationContext authContext)
|
IAuthorizationContext authContext)
|
||||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
: base(loggerFactory.CreateLogger<TrailersService>(), serverConfigurationManager, httpResultFactory)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
@ -56,6 +61,7 @@ namespace MediaBrowser.Api.Movies
|
||||||
_localizationManager = localizationManager;
|
_localizationManager = localizationManager;
|
||||||
_json = json;
|
_json = json;
|
||||||
_authContext = authContext;
|
_authContext = authContext;
|
||||||
|
_logger = loggerFactory.CreateLogger<ItemsService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public object Get(Getrailers request)
|
public object Get(Getrailers request)
|
||||||
|
@ -66,7 +72,7 @@ namespace MediaBrowser.Api.Movies
|
||||||
getItems.IncludeItemTypes = "Trailer";
|
getItems.IncludeItemTypes = "Trailer";
|
||||||
|
|
||||||
return new ItemsService(
|
return new ItemsService(
|
||||||
Logger,
|
_logger,
|
||||||
ServerConfigurationManager,
|
ServerConfigurationManager,
|
||||||
ResultFactory,
|
ResultFactory,
|
||||||
_userManager,
|
_userManager,
|
||||||
|
|
|
@ -82,7 +82,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
|
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected BaseStreamingService(
|
protected BaseStreamingService(
|
||||||
ILogger logger,
|
ILogger<BaseStreamingService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
|
|
@ -25,7 +25,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||||
public abstract class BaseHlsService : BaseStreamingService
|
public abstract class BaseHlsService : BaseStreamingService
|
||||||
{
|
{
|
||||||
public BaseHlsService(
|
public BaseHlsService(
|
||||||
ILogger logger,
|
ILogger<BaseHlsService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
|
|
@ -94,7 +94,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||||
public class DynamicHlsService : BaseHlsService
|
public class DynamicHlsService : BaseHlsService
|
||||||
{
|
{
|
||||||
public DynamicHlsService(
|
public DynamicHlsService(
|
||||||
ILogger logger,
|
ILogger<DynamicHlsService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
|
|
@ -80,7 +80,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
private readonly IAuthorizationContext _authContext;
|
private readonly IAuthorizationContext _authContext;
|
||||||
|
|
||||||
public MediaInfoService(
|
public MediaInfoService(
|
||||||
ILogger logger,
|
ILogger<MediaInfoService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
|
|
|
@ -33,7 +33,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
public class AudioService : BaseProgressiveStreamingService
|
public class AudioService : BaseProgressiveStreamingService
|
||||||
{
|
{
|
||||||
public AudioService(
|
public AudioService(
|
||||||
ILogger logger,
|
ILogger<AudioService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
protected IHttpClient HttpClient { get; private set; }
|
protected IHttpClient HttpClient { get; private set; }
|
||||||
|
|
||||||
public BaseProgressiveStreamingService(
|
public BaseProgressiveStreamingService(
|
||||||
ILogger logger,
|
ILogger<BaseProgressiveStreamingService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
|
|
|
@ -75,9 +75,11 @@ namespace MediaBrowser.Api.Playback
|
||||||
public class UniversalAudioService : BaseApiService
|
public class UniversalAudioService : BaseApiService
|
||||||
{
|
{
|
||||||
private readonly EncodingHelper _encodingHelper;
|
private readonly EncodingHelper _encodingHelper;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
||||||
public UniversalAudioService(
|
public UniversalAudioService(
|
||||||
ILogger<UniversalAudioService> logger,
|
ILogger<UniversalAudioService> logger,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
|
@ -108,6 +110,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
AuthorizationContext = authorizationContext;
|
AuthorizationContext = authorizationContext;
|
||||||
NetworkManager = networkManager;
|
NetworkManager = networkManager;
|
||||||
_encodingHelper = encodingHelper;
|
_encodingHelper = encodingHelper;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IHttpClient HttpClient { get; private set; }
|
protected IHttpClient HttpClient { get; private set; }
|
||||||
|
@ -233,7 +236,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
|
AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
|
||||||
|
|
||||||
var mediaInfoService = new MediaInfoService(
|
var mediaInfoService = new MediaInfoService(
|
||||||
Logger,
|
_loggerFactory.CreateLogger<MediaInfoService>(),
|
||||||
ServerConfigurationManager,
|
ServerConfigurationManager,
|
||||||
ResultFactory,
|
ResultFactory,
|
||||||
MediaSourceManager,
|
MediaSourceManager,
|
||||||
|
@ -277,7 +280,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var service = new DynamicHlsService(
|
var service = new DynamicHlsService(
|
||||||
Logger,
|
_loggerFactory.CreateLogger<DynamicHlsService>(),
|
||||||
ServerConfigurationManager,
|
ServerConfigurationManager,
|
||||||
ResultFactory,
|
ResultFactory,
|
||||||
UserManager,
|
UserManager,
|
||||||
|
@ -331,7 +334,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var service = new AudioService(
|
var service = new AudioService(
|
||||||
Logger,
|
_loggerFactory.CreateLogger<AudioService>(),
|
||||||
ServerConfigurationManager,
|
ServerConfigurationManager,
|
||||||
ResultFactory,
|
ResultFactory,
|
||||||
HttpClient,
|
HttpClient,
|
||||||
|
|
|
@ -31,46 +31,46 @@ namespace MediaBrowser.Api.Sessions
|
||||||
{
|
{
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
|
|
||||||
_sessionManager.SessionStarted += _sessionManager_SessionStarted;
|
_sessionManager.SessionStarted += OnSessionManagerSessionStarted;
|
||||||
_sessionManager.SessionEnded += _sessionManager_SessionEnded;
|
_sessionManager.SessionEnded += OnSessionManagerSessionEnded;
|
||||||
_sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
|
_sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
|
||||||
_sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
|
_sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
|
||||||
_sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress;
|
_sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress;
|
||||||
_sessionManager.CapabilitiesChanged += _sessionManager_CapabilitiesChanged;
|
_sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged;
|
||||||
_sessionManager.SessionActivity += _sessionManager_SessionActivity;
|
_sessionManager.SessionActivity += OnSessionManagerSessionActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sessionManager_SessionActivity(object sender, SessionEventArgs e)
|
private void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
SendData(false);
|
SendData(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sessionManager_CapabilitiesChanged(object sender, SessionEventArgs e)
|
private void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
SendData(true);
|
SendData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
private void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
||||||
{
|
{
|
||||||
SendData(!e.IsAutomated);
|
SendData(!e.IsAutomated);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
|
private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
|
||||||
{
|
{
|
||||||
SendData(true);
|
SendData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
|
private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
|
||||||
{
|
{
|
||||||
SendData(true);
|
SendData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
|
private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
SendData(true);
|
SendData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sessionManager_SessionStarted(object sender, SessionEventArgs e)
|
private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
SendData(true);
|
SendData(true);
|
||||||
}
|
}
|
||||||
|
@ -84,15 +84,16 @@ namespace MediaBrowser.Api.Sessions
|
||||||
return Task.FromResult(_sessionManager.Sessions);
|
return Task.FromResult(_sessionManager.Sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Dispose(bool dispose)
|
protected override void Dispose(bool dispose)
|
||||||
{
|
{
|
||||||
_sessionManager.SessionStarted -= _sessionManager_SessionStarted;
|
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
|
||||||
_sessionManager.SessionEnded -= _sessionManager_SessionEnded;
|
_sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
|
||||||
_sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
|
_sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
|
||||||
_sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
|
_sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
|
||||||
_sessionManager.PlaybackProgress -= _sessionManager_PlaybackProgress;
|
_sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
|
||||||
_sessionManager.CapabilitiesChanged -= _sessionManager_CapabilitiesChanged;
|
_sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
|
||||||
_sessionManager.SessionActivity -= _sessionManager_SessionActivity;
|
_sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
|
||||||
|
|
||||||
base.Dispose(dispose);
|
base.Dispose(dispose);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
|
@ -53,7 +55,10 @@ namespace MediaBrowser.Api.System
|
||||||
(DateTime?)null :
|
(DateTime?)null :
|
||||||
DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
|
DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
|
||||||
|
|
||||||
var result = _activityManager.GetPagedResult(request.StartIndex, request.Limit);
|
var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
|
||||||
|
entries => entries.Where(entry => entry.DateCreated >= minDate));
|
||||||
|
|
||||||
|
var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
|
||||||
|
|
||||||
return ToOptimizedResult(result);
|
return ToOptimizedResult(result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
|
@ -10,7 +10,7 @@ namespace MediaBrowser.Api.System
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class SessionInfoWebSocketListener
|
/// Class SessionInfoWebSocketListener
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
|
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name.
|
/// Gets the name.
|
||||||
|
@ -26,10 +26,10 @@ namespace MediaBrowser.Api.System
|
||||||
public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
|
public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
|
||||||
{
|
{
|
||||||
_activityManager = activityManager;
|
_activityManager = activityManager;
|
||||||
_activityManager.EntryCreated += _activityManager_EntryCreated;
|
_activityManager.EntryCreated += OnEntryCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
|
private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
|
||||||
{
|
{
|
||||||
SendData(true);
|
SendData(true);
|
||||||
}
|
}
|
||||||
|
@ -38,15 +38,15 @@ namespace MediaBrowser.Api.System
|
||||||
/// Gets the data to send.
|
/// Gets the data to send.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Task{SystemInfo}.</returns>
|
/// <returns>Task{SystemInfo}.</returns>
|
||||||
protected override Task<List<ActivityLogEntry>> GetDataToSend()
|
protected override Task<ActivityLogEntry[]> GetDataToSend()
|
||||||
{
|
{
|
||||||
return Task.FromResult(new List<ActivityLogEntry>());
|
return Task.FromResult(Array.Empty<ActivityLogEntry>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Dispose(bool dispose)
|
protected override void Dispose(bool dispose)
|
||||||
{
|
{
|
||||||
_activityManager.EntryCreated -= _activityManager_EntryCreated;
|
_activityManager.EntryCreated -= OnEntryCreated;
|
||||||
|
|
||||||
base.Dispose(dispose);
|
base.Dispose(dispose);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
public class ArtistsService : BaseItemsByNameService<MusicArtist>
|
public class ArtistsService : BaseItemsByNameService<MusicArtist>
|
||||||
{
|
{
|
||||||
public ArtistsService(
|
public ArtistsService(
|
||||||
ILogger<GenresService> logger,
|
ILogger<ArtistsService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
/// <param name="userDataRepository">The user data repository.</param>
|
/// <param name="userDataRepository">The user data repository.</param>
|
||||||
/// <param name="dtoService">The dto service.</param>
|
/// <param name="dtoService">The dto service.</param>
|
||||||
protected BaseItemsByNameService(
|
protected BaseItemsByNameService(
|
||||||
ILogger logger,
|
ILogger<BaseItemsByNameService<TItemType>> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
|
|
@ -60,7 +60,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
/// <param name="localization">The localization.</param>
|
/// <param name="localization">The localization.</param>
|
||||||
/// <param name="dtoService">The dto service.</param>
|
/// <param name="dtoService">The dto service.</param>
|
||||||
public ItemsService(
|
public ItemsService(
|
||||||
ILogger logger,
|
ILogger<ItemsService> logger,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
IHttpResultFactory httpResultFactory,
|
IHttpResultFactory httpResultFactory,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Model.Devices;
|
using MediaBrowser.Model.Devices;
|
||||||
using MediaBrowser.Model.Events;
|
using MediaBrowser.Model.Events;
|
||||||
|
@ -11,11 +9,6 @@ namespace MediaBrowser.Controller.Devices
|
||||||
{
|
{
|
||||||
public interface IDeviceManager
|
public interface IDeviceManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [camera image uploaded].
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves the capabilities.
|
/// Saves the capabilities.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -45,22 +38,6 @@ namespace MediaBrowser.Controller.Devices
|
||||||
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
||||||
QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
|
QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the upload history.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="deviceId">The device identifier.</param>
|
|
||||||
/// <returns>ContentUploadHistory.</returns>
|
|
||||||
ContentUploadHistory GetCameraUploadHistory(string deviceId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Accepts the upload.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="deviceId">The device identifier.</param>
|
|
||||||
/// <param name="stream">The stream.</param>
|
|
||||||
/// <param name="file">The file.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether this instance [can access device] the specified user identifier.
|
/// Determines whether this instance [can access device] the specified user identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -39,10 +39,9 @@ namespace MediaBrowser.Controller
|
||||||
int HttpsPort { get; }
|
int HttpsPort { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether [supports HTTPS].
|
/// Gets a value indicating whether the server should listen on an HTTPS port.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value>
|
bool ListenWithHttps { get; }
|
||||||
bool EnableHttps { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance has update available.
|
/// Gets a value indicating whether this instance has update available.
|
||||||
|
@ -57,34 +56,50 @@ namespace MediaBrowser.Controller
|
||||||
string FriendlyName { get; }
|
string FriendlyName { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the local ip address.
|
/// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
|
||||||
|
/// to the API that should exist at the address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The local ip address.</value>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
|
||||||
|
/// <returns>A list containing all the local IP addresses of the server.</returns>
|
||||||
Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
|
Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the local API URL.
|
/// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
|
||||||
|
/// IP address that can be found via <see cref="GetLocalIpAddresses"/>. HTTPS will be preferred when available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">Token to cancel the request if needed.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
|
||||||
/// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
|
/// <returns>The server URL.</returns>
|
||||||
/// <value>The local API URL.</value>
|
Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
|
||||||
Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the local API URL.
|
/// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
|
||||||
|
/// over HTTP (not HTTPS).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hostname">The hostname.</param>
|
/// <returns>The API URL.</returns>
|
||||||
/// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
|
string GetLoopbackHttpApiUrl();
|
||||||
/// <returns>The local API URL.</returns>
|
|
||||||
string GetLocalApiUrl(ReadOnlySpan<char> hostname, bool forceHttp = false);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the local API URL.
|
/// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">The IP address.</param>
|
/// <param name="address">The IP address to use as the hostname in the URL.</param>
|
||||||
/// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
|
/// <returns>The API URL.</returns>
|
||||||
/// <returns>The local API URL.</returns>
|
string GetLocalApiUrl(IPAddress address);
|
||||||
string GetLocalApiUrl(IPAddress address, bool forceHttp = false);
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a local (LAN) URL that can be used to access the API.
|
||||||
|
/// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hostname">The hostname to use in the URL.</param>
|
||||||
|
/// <param name="scheme">
|
||||||
|
/// The scheme to use for the URL. If null, the scheme will be selected automatically,
|
||||||
|
/// preferring HTTPS, if available.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="port">
|
||||||
|
/// The port to use for the URL. If null, the port will be selected automatically,
|
||||||
|
/// preferring the HTTPS port, if available.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>The API URL.</returns>
|
||||||
|
string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Open a URL in an external browser window.
|
/// Open a URL in an external browser window.
|
||||||
|
@ -101,7 +116,5 @@ namespace MediaBrowser.Controller
|
||||||
string ReverseVirtualPath(string path);
|
string ReverseVirtualPath(string path);
|
||||||
|
|
||||||
Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
|
Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
|
||||||
|
|
||||||
Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,8 +77,6 @@ namespace MediaBrowser.Controller.Net
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts sending messages over a web socket
|
/// Starts sending messages over a web socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -87,12 +85,12 @@ namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
var vals = message.Data.Split(',');
|
var vals = message.Data.Split(',');
|
||||||
|
|
||||||
var dueTimeMs = long.Parse(vals[0], UsCulture);
|
var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture);
|
||||||
var periodMs = long.Parse(vals[1], UsCulture);
|
var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name);
|
Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name);
|
||||||
|
|
||||||
var state = new TStateType
|
var state = new TStateType
|
||||||
{
|
{
|
||||||
|
@ -154,7 +152,6 @@ namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
MessageType = Name,
|
MessageType = Name,
|
||||||
Data = data
|
Data = data
|
||||||
|
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
state.DateLastSendUtc = DateTime.UtcNow;
|
state.DateLastSendUtc = DateTime.UtcNow;
|
||||||
|
@ -197,7 +194,7 @@ namespace MediaBrowser.Controller.Net
|
||||||
/// <param name="connection">The connection.</param>
|
/// <param name="connection">The connection.</param>
|
||||||
private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
|
private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
|
Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
|
||||||
|
|
||||||
// TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
|
// TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
|
||||||
// connection.Item1.Dispose();
|
// connection.Item1.Dispose();
|
||||||
|
@ -242,6 +239,7 @@ namespace MediaBrowser.Controller.Net
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Model.Events;
|
using MediaBrowser.Model.Events;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
@ -9,9 +8,9 @@ using Microsoft.AspNetCore.Http;
|
||||||
namespace MediaBrowser.Controller.Net
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface IHttpServer
|
/// Interface IHttpServer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IHttpServer : IDisposable
|
public interface IHttpServer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the URL prefix.
|
/// Gets the URL prefix.
|
||||||
|
@ -19,11 +18,6 @@ namespace MediaBrowser.Controller.Net
|
||||||
/// <value>The URL prefix.</value>
|
/// <value>The URL prefix.</value>
|
||||||
string[] UrlPrefixes { get; }
|
string[] UrlPrefixes { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops this instance.
|
|
||||||
/// </summary>
|
|
||||||
void Stop();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [web socket connected].
|
/// Occurs when [web socket connected].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -39,23 +33,18 @@ namespace MediaBrowser.Controller.Net
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string GlobalResponse { get; set; }
|
string GlobalResponse { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends the http context to the socket listener
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ctx"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task ProcessWebSocketRequest(HttpContext ctx);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The HTTP request handler
|
/// The HTTP request handler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq"></param>
|
/// <param name="context"></param>
|
||||||
/// <param name="urlString"></param>
|
|
||||||
/// <param name="host"></param>
|
|
||||||
/// <param name="localPath"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath,
|
Task RequestHandler(HttpContext context);
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the default CORS headers
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="req"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IDictionary<string, string> GetDefaultCorsHeaders(IRequest req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -7,18 +10,12 @@ using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Net
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
public interface IWebSocketConnection : IDisposable
|
public interface IWebSocketConnection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [closed].
|
/// Occurs when [closed].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<EventArgs> Closed;
|
event EventHandler<EventArgs>? Closed;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the id.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The id.</value>
|
|
||||||
Guid Id { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the last activity date.
|
/// Gets the last activity date.
|
||||||
|
@ -26,22 +23,17 @@ namespace MediaBrowser.Controller.Net
|
||||||
/// <value>The last activity date.</value>
|
/// <value>The last activity date.</value>
|
||||||
DateTime LastActivityDate { get; }
|
DateTime LastActivityDate { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the URL.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The URL.</value>
|
|
||||||
string Url { get; set; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the query string.
|
/// Gets or sets the query string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The query string.</value>
|
/// <value>The query string.</value>
|
||||||
IQueryCollection QueryString { get; set; }
|
IQueryCollection QueryString { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the receive action.
|
/// Gets or sets the receive action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The receive action.</value>
|
/// <value>The receive action.</value>
|
||||||
Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
|
Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the state.
|
/// Gets the state.
|
||||||
|
@ -53,7 +45,7 @@ namespace MediaBrowser.Controller.Net
|
||||||
/// Gets the remote end point.
|
/// Gets the remote end point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The remote end point.</value>
|
/// <value>The remote end point.</value>
|
||||||
string RemoteEndPoint { get; }
|
IPAddress? RemoteEndPoint { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a message asynchronously.
|
/// Sends a message asynchronously.
|
||||||
|
@ -65,21 +57,6 @@ namespace MediaBrowser.Controller.Net
|
||||||
/// <exception cref="ArgumentNullException">message</exception>
|
/// <exception cref="ArgumentNullException">message</exception>
|
||||||
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
|
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
Task ProcessAsync(CancellationToken cancellationToken = default);
|
||||||
/// Sends a message asynchronously.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buffer">The buffer.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task SendAsync(byte[] buffer, CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a message asynchronously.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">The text.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">buffer</exception>
|
|
||||||
Task SendAsync(string text, CancellationToken cancellationToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -20,6 +21,6 @@ namespace MediaBrowser.Controller.Session
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends the message.
|
/// Sends the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken);
|
Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging;
|
||||||
namespace MediaBrowser.Controller.Session
|
namespace MediaBrowser.Controller.Session
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class SessionInfo
|
/// Class SessionInfo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SessionInfo : IDisposable
|
public sealed class SessionInfo : IDisposable
|
||||||
{
|
{
|
||||||
private ISessionManager _sessionManager;
|
// 1 second
|
||||||
|
private const long ProgressIncrement = 10000000;
|
||||||
|
|
||||||
|
private readonly ISessionManager _sessionManager;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly object _progressLock = new object();
|
||||||
|
private Timer _progressTimer;
|
||||||
|
private PlaybackProgressInfo _lastProgressInfo;
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
public SessionInfo(ISessionManager sessionManager, ILogger logger)
|
public SessionInfo(ISessionManager sessionManager, ILogger logger)
|
||||||
{
|
{
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
|
@ -97,8 +107,6 @@ namespace MediaBrowser.Controller.Session
|
||||||
/// <value>The name of the device.</value>
|
/// <value>The name of the device.</value>
|
||||||
public string DeviceName { get; set; }
|
public string DeviceName { get; set; }
|
||||||
|
|
||||||
public string DeviceType { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the now playing item.
|
/// Gets or sets the now playing item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -128,22 +136,6 @@ namespace MediaBrowser.Controller.Session
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public ISessionController[] SessionControllers { get; set; }
|
public ISessionController[] SessionControllers { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the supported commands.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The supported commands.</value>
|
|
||||||
public string[] SupportedCommands
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (Capabilities == null)
|
|
||||||
{
|
|
||||||
return new string[] { };
|
|
||||||
}
|
|
||||||
return Capabilities.SupportedCommands;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TranscodingInfo TranscodingInfo { get; set; }
|
public TranscodingInfo TranscodingInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -215,6 +207,14 @@ namespace MediaBrowser.Controller.Session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public QueueItem[] NowPlayingQueue { get; set; }
|
||||||
|
|
||||||
|
public bool HasCustomDeviceName { get; set; }
|
||||||
|
|
||||||
|
public string PlaylistItemId { get; set; }
|
||||||
|
|
||||||
|
public string UserPrimaryImageTag { get; set; }
|
||||||
|
|
||||||
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
|
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
|
||||||
{
|
{
|
||||||
var controllers = SessionControllers.ToList();
|
var controllers = SessionControllers.ToList();
|
||||||
|
@ -258,10 +258,6 @@ namespace MediaBrowser.Controller.Session
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly object _progressLock = new object();
|
|
||||||
private Timer _progressTimer;
|
|
||||||
private PlaybackProgressInfo _lastProgressInfo;
|
|
||||||
|
|
||||||
public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
|
public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
@ -284,9 +280,6 @@ namespace MediaBrowser.Controller.Session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1 second
|
|
||||||
private const long ProgressIncrement = 10000000;
|
|
||||||
|
|
||||||
private async void OnProgressTimerCallback(object state)
|
private async void OnProgressTimerCallback(object state)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
@ -345,8 +338,7 @@ namespace MediaBrowser.Controller.Session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _disposed = false;
|
/// <inheritdoc />
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
@ -358,30 +350,12 @@ namespace MediaBrowser.Controller.Session
|
||||||
|
|
||||||
foreach (var controller in controllers)
|
foreach (var controller in controllers)
|
||||||
{
|
{
|
||||||
var disposable = controller as IDisposable;
|
if (controller is IDisposable disposable)
|
||||||
|
|
||||||
if (disposable != null)
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
|
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
|
||||||
|
disposable.Dispose();
|
||||||
try
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error disposing session controller");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_sessionManager = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueueItem[] NowPlayingQueue { get; set; }
|
|
||||||
public bool HasCustomDeviceName { get; set; }
|
|
||||||
public string PlaylistItemId { get; set; }
|
|
||||||
public string ServerId { get; set; }
|
|
||||||
public string UserPrimaryImageTag { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,25 @@ using MediaBrowser.Model.MediaInfo;
|
||||||
|
|
||||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Subtitle writer for the WebVTT format.
|
||||||
|
/// </summary>
|
||||||
public class VttWriter : ISubtitleWriter
|
public class VttWriter : ISubtitleWriter
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||||
{
|
{
|
||||||
writer.WriteLine("WEBVTT");
|
writer.WriteLine("WEBVTT");
|
||||||
writer.WriteLine(string.Empty);
|
writer.WriteLine(string.Empty);
|
||||||
|
writer.WriteLine("REGION");
|
||||||
|
writer.WriteLine("id:subtitle");
|
||||||
|
writer.WriteLine("width:80%");
|
||||||
|
writer.WriteLine("lines:3");
|
||||||
|
writer.WriteLine("regionanchor:50%,100%");
|
||||||
|
writer.WriteLine("viewportanchor:50%,90%");
|
||||||
|
writer.WriteLine(string.Empty);
|
||||||
foreach (var trackEvent in info.TrackEvents)
|
foreach (var trackEvent in info.TrackEvents)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
@ -22,13 +33,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||||
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks);
|
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks);
|
||||||
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks);
|
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks);
|
||||||
|
|
||||||
// make sure the start and end times are different and seqential
|
// make sure the start and end times are different and sequential
|
||||||
if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds)
|
if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds)
|
||||||
{
|
{
|
||||||
endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
|
endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", startTime, endTime);
|
writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle", startTime, endTime);
|
||||||
|
|
||||||
var text = trackEvent.Text;
|
var text = trackEvent.Text;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
@ -21,7 +20,7 @@ namespace MediaBrowser.Model.Activity
|
||||||
QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit);
|
QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit);
|
||||||
|
|
||||||
QueryResult<ActivityLogEntry> GetPagedResult(
|
QueryResult<ActivityLogEntry> GetPagedResult(
|
||||||
Func<IQueryable<ActivityLog>, IEnumerable<ActivityLog>> func,
|
Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
|
||||||
int? startIndex,
|
int? startIndex,
|
||||||
int? limit);
|
int? limit);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,17 +49,24 @@ namespace MediaBrowser.Model.Configuration
|
||||||
public int HttpsPortNumber { get; set; }
|
public int HttpsPortNumber { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether [use HTTPS].
|
/// Gets or sets a value indicating whether to use HTTPS.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if [use HTTPS]; otherwise, <c>false</c>.</value>
|
/// <remarks>
|
||||||
|
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
|
||||||
|
/// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
|
||||||
|
/// </remarks>
|
||||||
public bool EnableHttps { get; set; }
|
public bool EnableHttps { get; set; }
|
||||||
|
|
||||||
public bool EnableNormalizedItemByNameIds { get; set; }
|
public bool EnableNormalizedItemByNameIds { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The value pointing to the file system where the ssl certificate is located..</value>
|
|
||||||
public string CertificatePath { get; set; }
|
public string CertificatePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
|
||||||
|
/// </summary>
|
||||||
public string CertificatePassword { get; set; }
|
public string CertificatePassword { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -69,8 +76,9 @@ namespace MediaBrowser.Model.Configuration
|
||||||
public bool IsPortAuthorized { get; set; }
|
public bool IsPortAuthorized { get; set; }
|
||||||
|
|
||||||
public bool AutoRunWebApp { get; set; }
|
public bool AutoRunWebApp { get; set; }
|
||||||
|
|
||||||
public bool EnableRemoteAccess { get; set; }
|
public bool EnableRemoteAccess { get; set; }
|
||||||
public bool CameraUploadUpgraded { get; set; }
|
|
||||||
public bool CollectionsUpgraded { get; set; }
|
public bool CollectionsUpgraded { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -86,6 +94,7 @@ namespace MediaBrowser.Model.Configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The metadata path.</value>
|
/// <value>The metadata path.</value>
|
||||||
public string MetadataPath { get; set; }
|
public string MetadataPath { get; set; }
|
||||||
|
|
||||||
public string MetadataNetworkPath { get; set; }
|
public string MetadataNetworkPath { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -208,15 +217,26 @@ namespace MediaBrowser.Model.Configuration
|
||||||
public int RemoteClientBitrateLimit { get; set; }
|
public int RemoteClientBitrateLimit { get; set; }
|
||||||
|
|
||||||
public bool EnableFolderView { get; set; }
|
public bool EnableFolderView { get; set; }
|
||||||
|
|
||||||
public bool EnableGroupingIntoCollections { get; set; }
|
public bool EnableGroupingIntoCollections { get; set; }
|
||||||
|
|
||||||
public bool DisplaySpecialsWithinSeasons { get; set; }
|
public bool DisplaySpecialsWithinSeasons { get; set; }
|
||||||
|
|
||||||
public string[] LocalNetworkSubnets { get; set; }
|
public string[] LocalNetworkSubnets { get; set; }
|
||||||
|
|
||||||
public string[] LocalNetworkAddresses { get; set; }
|
public string[] LocalNetworkAddresses { get; set; }
|
||||||
|
|
||||||
public string[] CodecsUsed { get; set; }
|
public string[] CodecsUsed { get; set; }
|
||||||
|
|
||||||
public bool IgnoreVirtualInterfaces { get; set; }
|
public bool IgnoreVirtualInterfaces { get; set; }
|
||||||
|
|
||||||
public bool EnableExternalContentInSuggestions { get; set; }
|
public bool EnableExternalContentInSuggestions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the server should force connections over HTTPS.
|
||||||
|
/// </summary>
|
||||||
public bool RequireHttps { get; set; }
|
public bool RequireHttps { get; set; }
|
||||||
public bool IsBehindProxy { get; set; }
|
|
||||||
public bool EnableNewOmdbSupport { get; set; }
|
public bool EnableNewOmdbSupport { get; set; }
|
||||||
|
|
||||||
public string[] RemoteIPFilter { get; set; }
|
public string[] RemoteIPFilter { get; set; }
|
||||||
|
|
9
MediaBrowser.Model/Devices/DeviceOptions.cs
Normal file
9
MediaBrowser.Model/Devices/DeviceOptions.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
namespace MediaBrowser.Model.Devices
|
||||||
|
{
|
||||||
|
public class DeviceOptions
|
||||||
|
{
|
||||||
|
public string CustomName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<string>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DeviceOptions
|
|
||||||
{
|
|
||||||
public string CustomName { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Net
|
namespace MediaBrowser.Model.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -13,7 +16,9 @@ namespace MediaBrowser.Model.Net
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The type of the message.</value>
|
/// <value>The type of the message.</value>
|
||||||
public string MessageType { get; set; }
|
public string MessageType { get; set; }
|
||||||
public string MessageId { get; set; }
|
|
||||||
|
public Guid MessageId { get; set; }
|
||||||
|
|
||||||
public string ServerId { get; set; }
|
public string ServerId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -22,5 +27,4 @@ namespace MediaBrowser.Model.Net
|
||||||
/// <value>The data.</value>
|
/// <value>The data.</value>
|
||||||
public T Data { get; set; }
|
public T Data { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,24 +115,6 @@ namespace MediaBrowser.Model.System
|
||||||
/// <value>The transcode path.</value>
|
/// <value>The transcode path.</value>
|
||||||
public string TranscodingTempPath { get; set; }
|
public string TranscodingTempPath { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the HTTP server port number.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The HTTP server port number.</value>
|
|
||||||
public int HttpServerPortNumber { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether [enable HTTPS].
|
|
||||||
/// </summary>
|
|
||||||
/// <value><c>true</c> if [enable HTTPS]; otherwise, <c>false</c>.</value>
|
|
||||||
public bool SupportsHttps { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the HTTPS server port number.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The HTTPS server port number.</value>
|
|
||||||
public int HttpsPortNumber { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether this instance has update available.
|
/// Gets or sets a value indicating whether this instance has update available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -11,13 +11,10 @@ using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Plugins.Omdb
|
namespace MediaBrowser.Providers.Plugins.Omdb
|
||||||
{
|
{
|
||||||
public class OmdbEpisodeProvider :
|
public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
||||||
IRemoteMetadataProvider<Episode, EpisodeInfo>,
|
|
||||||
IHasOrder
|
|
||||||
{
|
{
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
|
@ -26,16 +23,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
private readonly IApplicationHost _appHost;
|
private readonly IApplicationHost _appHost;
|
||||||
|
|
||||||
public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
|
public OmdbEpisodeProvider(
|
||||||
|
IJsonSerializer jsonSerializer,
|
||||||
|
IApplicationHost appHost,
|
||||||
|
IHttpClient httpClient,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IServerConfigurationManager configurationManager)
|
||||||
{
|
{
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, logger, libraryManager, fileSystem, configurationManager);
|
_itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, libraryManager, fileSystem, configurationManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After TheTvDb
|
||||||
|
public int Order => 1;
|
||||||
|
|
||||||
|
public string Name => "The Open Movie Database";
|
||||||
|
|
||||||
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken);
|
return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken);
|
||||||
|
@ -66,10 +74,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// After TheTvDb
|
|
||||||
public int Order => 1;
|
|
||||||
|
|
||||||
public string Name => "The Open Movie Database";
|
|
||||||
|
|
||||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,7 +17,6 @@ using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Plugins.Omdb
|
namespace MediaBrowser.Providers.Plugins.Omdb
|
||||||
{
|
{
|
||||||
|
@ -26,22 +25,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||||
{
|
{
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
private readonly IApplicationHost _appHost;
|
private readonly IApplicationHost _appHost;
|
||||||
|
|
||||||
public OmdbItemProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
|
public OmdbItemProvider(
|
||||||
|
IJsonSerializer jsonSerializer,
|
||||||
|
IApplicationHost appHost,
|
||||||
|
IHttpClient httpClient,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IServerConfigurationManager configurationManager)
|
||||||
{
|
{
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
// After primary option
|
// After primary option
|
||||||
public int Order => 2;
|
public int Order => 2;
|
||||||
|
|
||||||
|
@ -80,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||||
var parsedName = _libraryManager.ParseName(name);
|
var parsedName = _libraryManager.ParseName(name);
|
||||||
var yearInName = parsedName.Year;
|
var yearInName = parsedName.Year;
|
||||||
name = parsedName.Name;
|
name = parsedName.Name;
|
||||||
year = year ?? yearInName;
|
year ??= yearInName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(imdbId))
|
if (string.IsNullOrWhiteSpace(imdbId))
|
||||||
|
@ -312,6 +316,5 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||||
/// <value>The results.</value>
|
/// <value>The results.</value>
|
||||||
public List<SearchResult> Search { get; set; }
|
public List<SearchResult> Search { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,6 @@ namespace MediaBrowser.Providers.Tmdb.TV
|
||||||
public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
|
public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
|
||||||
{
|
{
|
||||||
private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
|
private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
|
||||||
|
|
||||||
internal static TmdbSeriesProvider Current { get; private set; }
|
|
||||||
|
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
@ -39,6 +36,10 @@ namespace MediaBrowser.Providers.Tmdb.TV
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
|
internal static TmdbSeriesProvider Current { get; private set; }
|
||||||
|
|
||||||
public TmdbSeriesProvider(
|
public TmdbSeriesProvider(
|
||||||
IJsonSerializer jsonSerializer,
|
IJsonSerializer jsonSerializer,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
|
@ -217,10 +218,9 @@ namespace MediaBrowser.Providers.Tmdb.TV
|
||||||
var series = seriesResult.Item;
|
var series = seriesResult.Item;
|
||||||
|
|
||||||
series.Name = seriesInfo.Name;
|
series.Name = seriesInfo.Name;
|
||||||
|
series.OriginalTitle = seriesInfo.Original_Name;
|
||||||
series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture));
|
series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture));
|
||||||
|
|
||||||
//series.VoteCount = seriesInfo.vote_count;
|
|
||||||
|
|
||||||
string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
|
string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
|
if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
|
||||||
|
@ -240,7 +240,7 @@ namespace MediaBrowser.Providers.Tmdb.TV
|
||||||
series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
|
series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
//series.HomePageUrl = seriesInfo.homepage;
|
series.HomePageUrl = seriesInfo.Homepage;
|
||||||
|
|
||||||
series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
|
series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
|
||||||
|
|
||||||
|
@ -308,29 +308,61 @@ namespace MediaBrowser.Providers.Tmdb.TV
|
||||||
seriesResult.ResetPeople();
|
seriesResult.ResetPeople();
|
||||||
var tmdbImageUrl = settings.images.GetImageUrl("original");
|
var tmdbImageUrl = settings.images.GetImageUrl("original");
|
||||||
|
|
||||||
if (seriesInfo.Credits != null && seriesInfo.Credits.Cast != null)
|
if (seriesInfo.Credits != null)
|
||||||
{
|
{
|
||||||
foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
|
if (seriesInfo.Credits.Cast != null)
|
||||||
{
|
{
|
||||||
var personInfo = new PersonInfo
|
foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
|
||||||
{
|
{
|
||||||
Name = actor.Name.Trim(),
|
var personInfo = new PersonInfo
|
||||||
Role = actor.Character,
|
{
|
||||||
Type = PersonType.Actor,
|
Name = actor.Name.Trim(),
|
||||||
SortOrder = actor.Order
|
Role = actor.Character,
|
||||||
|
Type = PersonType.Actor,
|
||||||
|
SortOrder = actor.Order
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
|
||||||
|
{
|
||||||
|
personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.Id > 0)
|
||||||
|
{
|
||||||
|
personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesResult.AddPerson(personInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seriesInfo.Credits.Crew != null)
|
||||||
|
{
|
||||||
|
var keepTypes = new[]
|
||||||
|
{
|
||||||
|
PersonType.Director,
|
||||||
|
PersonType.Writer,
|
||||||
|
PersonType.Producer
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
|
foreach (var person in seriesInfo.Credits.Crew)
|
||||||
{
|
{
|
||||||
personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
|
// Normalize this
|
||||||
}
|
var type = TmdbUtils.MapCrewToPersonType(person);
|
||||||
|
|
||||||
if (actor.Id > 0)
|
if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
&& !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||||
personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
|
{
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
seriesResult.AddPerson(personInfo);
|
seriesResult.AddPerson(new PersonInfo
|
||||||
|
{
|
||||||
|
Name = person.Name.Trim(),
|
||||||
|
Role = person.Job,
|
||||||
|
Type = type
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,51 @@ using MediaBrowser.Providers.Tmdb.Models.General;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Tmdb
|
namespace MediaBrowser.Providers.Tmdb
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Utilities for the TMDb provider
|
||||||
|
/// </summary>
|
||||||
public static class TmdbUtils
|
public static class TmdbUtils
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the TMDB instance to use.
|
||||||
|
/// </summary>
|
||||||
public const string BaseTmdbUrl = "https://www.themoviedb.org/";
|
public const string BaseTmdbUrl = "https://www.themoviedb.org/";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the TMDB API instance to use.
|
||||||
|
/// </summary>
|
||||||
public const string BaseTmdbApiUrl = "https://api.themoviedb.org/";
|
public const string BaseTmdbApiUrl = "https://api.themoviedb.org/";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the provider.
|
||||||
|
/// </summary>
|
||||||
public const string ProviderName = "TheMovieDb";
|
public const string ProviderName = "TheMovieDb";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API key to use when performing an API call.
|
||||||
|
/// </summary>
|
||||||
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
|
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value of the Accept header for requests to the provider.
|
||||||
|
/// </summary>
|
||||||
public const string AcceptHeader = "application/json,image/*";
|
public const string AcceptHeader = "application/json,image/*";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps the TMDB provided roles for crew members to Jellyfin roles.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="crew">Crew member to map against the Jellyfin person types.</param>
|
||||||
|
/// <returns>The Jellyfin person type.</returns>
|
||||||
public static string MapCrewToPersonType(Crew crew)
|
public static string MapCrewToPersonType(Crew crew)
|
||||||
{
|
{
|
||||||
if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
|
if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
|
||||||
&& crew.Job.IndexOf("producer", StringComparison.InvariantCultureIgnoreCase) != -1)
|
&& crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return PersonType.Director;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
&& crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
return PersonType.Producer;
|
return PersonType.Producer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 16.0.30011.22
|
VisualStudioVersion = 15.0.26730.3
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
Loading…
Reference in New Issue
Block a user