aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs5
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs2
-rw-r--r--Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs268
-rw-r--r--Emby.Server.Implementations/Activity/ActivityManager.cs70
-rw-r--r--Emby.Server.Implementations/Activity/ActivityRepository.cs308
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs109
-rw-r--r--Emby.Server.Implementations/Browser/BrowserLauncher.cs8
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs6
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs288
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs222
-rw-r--r--Emby.Server.Implementations/HttpServer/IHttpListener.cs39
-rw-r--r--Emby.Server.Implementations/HttpServer/ResponseFilter.cs19
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs279
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json6
-rw-r--r--Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs39
-rw-r--r--Emby.Server.Implementations/Net/IWebSocket.cs48
-rw-r--r--Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs29
-rw-r--r--Emby.Server.Implementations/Session/HttpSessionController.cs191
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs15
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs38
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs86
-rw-r--r--Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs105
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs135
-rw-r--r--Emby.Server.Implementations/WebSockets/WebSocketHandler.cs10
-rw-r--r--Emby.Server.Implementations/WebSockets/WebSocketManager.cs102
-rw-r--r--Jellyfin.Data/Entities/ActivityLog.cs154
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj20
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs102
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj8
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs9
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbProvider.cs33
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs72
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs46
-rw-r--r--Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs20
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs66
-rw-r--r--Jellyfin.Server/CoreAppHost.cs14
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj1
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs133
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs79
-rw-r--r--Jellyfin.Server/Program.cs7
-rw-r--r--Jellyfin.Server/Startup.cs1
-rw-r--r--MediaBrowser.Api/BaseApiService.cs4
-rw-r--r--MediaBrowser.Api/Devices/DeviceService.cs36
-rw-r--r--MediaBrowser.Api/Library/LibraryService.cs15
-rw-r--r--MediaBrowser.Api/Movies/MoviesService.cs2
-rw-r--r--MediaBrowser.Api/Movies/TrailersService.cs12
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs2
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Progressive/AudioService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs2
-rw-r--r--MediaBrowser.Api/Playback/UniversalAudioService.cs9
-rw-r--r--MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs43
-rw-r--r--MediaBrowser.Api/System/ActivityLogService.cs7
-rw-r--r--MediaBrowser.Api/System/ActivityLogWebSocketListener.cs16
-rw-r--r--MediaBrowser.Api/UserLibrary/ArtistsService.cs2
-rw-r--r--MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs2
-rw-r--r--MediaBrowser.Api/UserLibrary/ItemsService.cs2
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs23
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs57
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs12
-rw-r--r--MediaBrowser.Controller/Net/IHttpServer.cs27
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs41
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs3
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs74
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs15
-rw-r--r--MediaBrowser.Model/Activity/ActivityLogEntry.cs1
-rw-r--r--MediaBrowser.Model/Activity/IActivityManager.cs14
-rw-r--r--MediaBrowser.Model/Activity/IActivityRepository.cs14
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs32
-rw-r--r--MediaBrowser.Model/Devices/DeviceOptions.cs9
-rw-r--r--MediaBrowser.Model/Devices/DevicesOptions.cs23
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj3
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs1
-rw-r--r--MediaBrowser.Model/Net/WebSocketMessage.cs8
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs18
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs24
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs15
-rw-r--r--MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs74
-rw-r--r--MediaBrowser.Providers/Tmdb/TmdbUtils.cs35
-rw-r--r--MediaBrowser.sln13
92 files changed, 1606 insertions, 2316 deletions
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 43e983054..9d7c0d365 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -908,7 +908,8 @@ namespace Emby.Dlna.PlayTo
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)
{
@@ -924,10 +925,12 @@ namespace Emby.Dlna.PlayTo
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
+
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
+
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 7f755fd25..948fe037b 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -227,7 +227,7 @@ namespace Emby.Naming.Video
}
return remainingFiles
- .Where(i => i.ExtraType == null)
+ .Where(i => i.ExtraType != null)
.Where(i => baseNames.Any(b =>
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
.ToList();
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
index 4685a03ac..3983824a3 100644
--- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
+++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
@@ -4,11 +4,10 @@ using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
@@ -30,7 +29,7 @@ namespace Emby.Server.Implementations.Activity
/// </summary>
public sealed class ActivityLogEntryPoint : IServerEntryPoint
{
- private readonly ILogger _logger;
+ private readonly ILogger<ActivityLogEntryPoint> _logger;
private readonly IInstallationManager _installationManager;
private readonly ISessionManager _sessionManager;
private readonly ITaskManager _taskManager;
@@ -38,14 +37,12 @@ namespace Emby.Server.Implementations.Activity
private readonly ILocalizationManager _localization;
private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager;
- private readonly IDeviceManager _deviceManager;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
- /// <param name="deviceManager">The device manager.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="activityManager">The activity manager.</param>
/// <param name="localization">The localization manager.</param>
@@ -55,7 +52,6 @@ namespace Emby.Server.Implementations.Activity
public ActivityLogEntryPoint(
ILogger<ActivityLogEntryPoint> logger,
ISessionManager sessionManager,
- IDeviceManager deviceManager,
ITaskManager taskManager,
IActivityManager activityManager,
ILocalizationManager localization,
@@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity
{
_logger = logger;
_sessionManager = sessionManager;
- _deviceManager = deviceManager;
_taskManager = taskManager;
_activityManager = activityManager;
_localization = localization;
@@ -99,52 +94,38 @@ namespace Emby.Server.Implementations.Activity
_userManager.UserPolicyUpdated += OnUserPolicyUpdated;
_userManager.UserLockedOut += OnUserLockedOut;
- _deviceManager.CameraImageUploaded += OnCameraImageUploaded;
-
return Task.CompletedTask;
}
- private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
+ private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("CameraImageUploadedFrom"),
- e.Argument.Device.Name),
- Type = NotificationType.CameraImageUploaded.ToString()
- });
- }
-
- private void OnUserLockedOut(object sender, GenericEventArgs<User> e)
- {
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserLockedOutWithName"),
- e.Argument.Name),
- Type = NotificationType.UserLockedOut.ToString(),
- UserId = e.Argument.Id
- });
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ _localization.GetLocalizedString("UserLockedOutWithName"),
+ e.Argument.Name),
+ NotificationType.UserLockedOut.ToString(),
+ e.Argument.Id))
+ .ConfigureAwait(false);
}
- private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
+ private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
e.Provider,
Notifications.NotificationEntryPoint.GetItemName(e.Item)),
- Type = "SubtitleDownloadFailure",
+ "SubtitleDownloadFailure",
+ Guid.Empty)
+ {
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
ShortOverview = e.Exception.Message
- });
+ }).ConfigureAwait(false);
}
- private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
+ private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
var item = e.MediaInfo;
@@ -167,20 +148,19 @@ namespace Emby.Server.Implementations.Activity
var user = e.Users[0];
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
user.Name,
GetItemName(item),
e.DeviceName),
- Type = GetPlaybackStoppedNotificationType(item.MediaType),
- UserId = user.Id
- });
+ GetPlaybackStoppedNotificationType(item.MediaType),
+ user.Id))
+ .ConfigureAwait(false);
}
- private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
+ private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
{
var item = e.MediaInfo;
@@ -203,17 +183,16 @@ namespace Emby.Server.Implementations.Activity
var user = e.Users.First();
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
user.Name,
GetItemName(item),
e.DeviceName),
- Type = GetPlaybackNotificationType(item.MediaType),
- UserId = user.Id
- });
+ GetPlaybackNotificationType(item.MediaType),
+ user.Id))
+ .ConfigureAwait(false);
}
private static string GetItemName(BaseItemDto item)
@@ -263,7 +242,7 @@ namespace Emby.Server.Implementations.Activity
return null;
}
- private void OnSessionEnded(object sender, SessionEventArgs e)
+ private async void OnSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
@@ -272,110 +251,108 @@ namespace Emby.Server.Implementations.Activity
return;
}
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOfflineFromDevice"),
session.UserName,
session.DeviceName),
- Type = "SessionEnded",
+ "SessionEnded",
+ session.UserId)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
session.RemoteEndPoint),
- UserId = session.UserId
- });
+ }).ConfigureAwait(false);
}
- private void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
+ private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
{
var user = e.Argument.User;
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
user.Name),
- Type = "AuthenticationSucceeded",
+ "AuthenticationSucceeded",
+ user.Id)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.SessionInfo.RemoteEndPoint),
- UserId = user.Id
- });
+ }).ConfigureAwait(false);
}
- private void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
+ private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
e.Argument.Username),
- Type = "AuthenticationFailed",
+ "AuthenticationFailed",
+ Guid.Empty)
+ {
+ LogSeverity = LogLevel.Error,
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.RemoteEndPoint),
- Severity = LogLevel.Error
- });
+ }).ConfigureAwait(false);
}
- private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
+ private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPolicyUpdatedWithName"),
e.Argument.Name),
- Type = "UserPolicyUpdated",
- UserId = e.Argument.Id
- });
+ "UserPolicyUpdated",
+ e.Argument.Id))
+ .ConfigureAwait(false);
}
- private void OnUserDeleted(object sender, GenericEventArgs<User> e)
+ private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserDeletedWithName"),
e.Argument.Name),
- Type = "UserDeleted"
- });
+ "UserDeleted",
+ Guid.Empty))
+ .ConfigureAwait(false);
}
- private void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
+ private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPasswordChangedWithName"),
e.Argument.Name),
- Type = "UserPasswordChanged",
- UserId = e.Argument.Id
- });
+ "UserPasswordChanged",
+ e.Argument.Id))
+ .ConfigureAwait(false);
}
- private void OnUserCreated(object sender, GenericEventArgs<User> e)
+ private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserCreatedWithName"),
e.Argument.Name),
- Type = "UserCreated",
- UserId = e.Argument.Id
- });
+ "UserCreated",
+ e.Argument.Id))
+ .ConfigureAwait(false);
}
- private void OnSessionStarted(object sender, SessionEventArgs e)
+ private async void OnSessionStarted(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
@@ -384,87 +361,90 @@ namespace Emby.Server.Implementations.Activity
return;
}
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOnlineFromDevice"),
session.UserName,
session.DeviceName),
- Type = "SessionStarted",
+ "SessionStarted",
+ session.UserId)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
- session.RemoteEndPoint),
- UserId = session.UserId
- });
+ session.RemoteEndPoint)
+ }).ConfigureAwait(false);
}
- private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
+ private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUpdatedWithName"),
e.Argument.Item1.Name),
- Type = NotificationType.PluginUpdateInstalled.ToString(),
+ NotificationType.PluginUpdateInstalled.ToString(),
+ Guid.Empty)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
e.Argument.Item2.version),
Overview = e.Argument.Item2.changelog
- });
+ }).ConfigureAwait(false);
}
- private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
+ private async void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUninstalledWithName"),
e.Argument.Name),
- Type = NotificationType.PluginUninstalled.ToString()
- });
+ NotificationType.PluginUninstalled.ToString(),
+ Guid.Empty))
+ .ConfigureAwait(false);
}
- private void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
+ private async void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginInstalledWithName"),
e.Argument.name),
- Type = NotificationType.PluginInstalled.ToString(),
+ NotificationType.PluginInstalled.ToString(),
+ Guid.Empty)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
e.Argument.version)
- });
+ }).ConfigureAwait(false);
}
- private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
+ private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
{
var installationInfo = e.InstallationInfo;
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameInstallFailed"),
installationInfo.Name),
- Type = NotificationType.InstallationFailed.ToString(),
+ NotificationType.InstallationFailed.ToString(),
+ Guid.Empty)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
installationInfo.Version),
Overview = e.Exception.Message
- });
+ }).ConfigureAwait(false);
}
- private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+ private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
{
var result = e.Result;
var task = e.Task;
@@ -495,22 +475,20 @@ namespace Emby.Server.Implementations.Activity
vals.Add(e.Result.LongErrorMessage);
}
- CreateLogEntry(new ActivityLogEntry
+ await CreateLogEntry(new ActivityLog(
+ string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
+ NotificationType.TaskFailed.ToString(),
+ Guid.Empty)
{
- Name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("ScheduledTaskFailedWithName"),
- task.Name),
- Type = NotificationType.TaskFailed.ToString(),
+ LogSeverity = LogLevel.Error,
Overview = string.Join(Environment.NewLine, vals),
- ShortOverview = runningTime,
- Severity = LogLevel.Error
- });
+ ShortOverview = runningTime
+ }).ConfigureAwait(false);
}
}
- private void CreateLogEntry(ActivityLogEntry entry)
- => _activityManager.Create(entry);
+ private async Task CreateLogEntry(ActivityLog entry)
+ => await _activityManager.CreateAsync(entry).ConfigureAwait(false);
/// <inheritdoc />
public void Dispose()
@@ -537,8 +515,6 @@ namespace Emby.Server.Implementations.Activity
_userManager.UserDeleted -= OnUserDeleted;
_userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
_userManager.UserLockedOut -= OnUserLockedOut;
-
- _deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
}
/// <summary>
@@ -566,7 +542,7 @@ namespace Emby.Server.Implementations.Activity
{
int months = days / DaysInMonth;
values.Add(CreateValueString(months, "month"));
- days %= DaysInMonth;
+ days = days % DaysInMonth;
}
// Number of days
diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs
deleted file mode 100644
index 81bebae3d..000000000
--- a/Emby.Server.Implementations/Activity/ActivityManager.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Querying;
-
-namespace Emby.Server.Implementations.Activity
-{
- /// <summary>
- /// The activity log manager.
- /// </summary>
- public class ActivityManager : IActivityManager
- {
- private readonly IActivityRepository _repo;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ActivityManager"/> class.
- /// </summary>
- /// <param name="repo">The activity repository.</param>
- /// <param name="userManager">The user manager.</param>
- public ActivityManager(IActivityRepository repo, IUserManager userManager)
- {
- _repo = repo;
- _userManager = userManager;
- }
-
- /// <inheritdoc />
- public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
-
- public void Create(ActivityLogEntry entry)
- {
- entry.Date = DateTime.UtcNow;
-
- _repo.Create(entry);
-
- EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(entry));
- }
-
- /// <inheritdoc />
- public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
- {
- var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
-
- foreach (var item in result.Items)
- {
- if (item.UserId == Guid.Empty)
- {
- continue;
- }
-
- var user = _userManager.GetUserById(item.UserId);
-
- if (user != null)
- {
- var dto = _userManager.GetUserDto(user);
- item.UserPrimaryImageTag = dto.PrimaryImageTag;
- }
- }
-
- return result;
- }
-
- /// <inheritdoc />
- public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit)
- {
- return GetActivityLogEntries(minDate, null, startIndex, limit);
- }
- }
-}
diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs
deleted file mode 100644
index 22796ba3f..000000000
--- a/Emby.Server.Implementations/Activity/ActivityRepository.cs
+++ /dev/null
@@ -1,308 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using Emby.Server.Implementations.Data;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Activity
-{
- /// <summary>
- /// The activity log repository.
- /// </summary>
- public class ActivityRepository : BaseSqliteRepository, IActivityRepository
- {
- private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog";
-
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ActivityRepository"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="appPaths">The server application paths.</param>
- /// <param name="fileSystem">The filesystem.</param>
- public ActivityRepository(ILogger<ActivityRepository> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem)
- : base(logger)
- {
- DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db");
- _fileSystem = fileSystem;
- }
-
- /// <summary>
- /// Initializes the <see cref="ActivityRepository"/>.
- /// </summary>
- public void Initialize()
- {
- try
- {
- InitializeInternal();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error loading database file. Will reset and retry.");
-
- _fileSystem.DeleteFile(DbFilePath);
-
- InitializeInternal();
- }
- }
-
- private void InitializeInternal()
- {
- using var connection = GetConnection();
- connection.RunQueries(new[]
- {
- "create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)",
- "drop index if exists idx_ActivityLogEntries"
- });
-
- TryMigrate(connection);
- }
-
- private void TryMigrate(ManagedConnection connection)
- {
- try
- {
- if (TableExists(connection, "ActivityLogEntries"))
- {
- connection.RunQueries(new[]
- {
- "INSERT INTO ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) SELECT Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity FROM ActivityLogEntries",
- "drop table if exists ActivityLogEntries"
- });
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error migrating activity log database");
- }
- }
-
- /// <inheritdoc />
- public void Create(ActivityLogEntry entry)
- {
- if (entry == null)
- {
- throw new ArgumentNullException(nameof(entry));
- }
-
- using var connection = GetConnection();
- connection.RunInTransaction(db =>
- {
- using var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)");
- statement.TryBind("@Name", entry.Name);
-
- statement.TryBind("@Overview", entry.Overview);
- statement.TryBind("@ShortOverview", entry.ShortOverview);
- statement.TryBind("@Type", entry.Type);
- statement.TryBind("@ItemId", entry.ItemId);
-
- if (entry.UserId.Equals(Guid.Empty))
- {
- statement.TryBindNull("@UserId");
- }
- else
- {
- statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
- }
-
- statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
- statement.TryBind("@LogSeverity", entry.Severity.ToString());
-
- statement.MoveNext();
- }, TransactionMode);
- }
-
- /// <summary>
- /// Adds the provided <see cref="ActivityLogEntry"/> to this repository.
- /// </summary>
- /// <param name="entry">The activity log entry.</param>
- /// <exception cref="ArgumentNullException">If entry is null.</exception>
- public void Update(ActivityLogEntry entry)
- {
- if (entry == null)
- {
- throw new ArgumentNullException(nameof(entry));
- }
-
- using var connection = GetConnection();
- connection.RunInTransaction(db =>
- {
- using var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id");
- statement.TryBind("@Id", entry.Id);
-
- statement.TryBind("@Name", entry.Name);
- statement.TryBind("@Overview", entry.Overview);
- statement.TryBind("@ShortOverview", entry.ShortOverview);
- statement.TryBind("@Type", entry.Type);
- statement.TryBind("@ItemId", entry.ItemId);
-
- if (entry.UserId.Equals(Guid.Empty))
- {
- statement.TryBindNull("@UserId");
- }
- else
- {
- statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
- }
-
- statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
- statement.TryBind("@LogSeverity", entry.Severity.ToString());
-
- statement.MoveNext();
- }, TransactionMode);
- }
-
- /// <inheritdoc />
- public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
- {
- var commandText = BaseActivitySelectText;
- var whereClauses = new List<string>();
-
- if (minDate.HasValue)
- {
- whereClauses.Add("DateCreated>=@DateCreated");
- }
-
- if (hasUserId.HasValue)
- {
- whereClauses.Add(hasUserId.Value ? "UserId not null" : "UserId is null");
- }
-
- var whereTextWithoutPaging = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- if (startIndex.HasValue && startIndex.Value > 0)
- {
- var pagingWhereText = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- whereClauses.Add(
- string.Format(
- CultureInfo.InvariantCulture,
- "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})",
- pagingWhereText,
- startIndex.Value));
- }
-
- var whereText = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- commandText += whereText;
-
- commandText += " ORDER BY DateCreated DESC";
-
- if (limit.HasValue)
- {
- commandText += " LIMIT " + limit.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- var statementTexts = new[]
- {
- commandText,
- "select count (Id) from ActivityLog" + whereTextWithoutPaging
- };
-
- var list = new List<ActivityLogEntry>();
- var result = new QueryResult<ActivityLogEntry>();
-
- using var connection = GetConnection(true);
- connection.RunInTransaction(
- db =>
- {
- var statements = PrepareAll(db, statementTexts).ToList();
-
- using (var statement = statements[0])
- {
- if (minDate.HasValue)
- {
- statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
- }
-
- list.AddRange(statement.ExecuteQuery().Select(GetEntry));
- }
-
- using (var statement = statements[1])
- {
- if (minDate.HasValue)
- {
- statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
- }
-
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
- }
- },
- ReadTransactionMode);
-
- result.Items = list;
- return result;
- }
-
- private static ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader)
- {
- var index = 0;
-
- var info = new ActivityLogEntry
- {
- Id = reader[index].ToInt64()
- };
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.Name = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.Overview = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.ShortOverview = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.Type = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.ItemId = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.UserId = new Guid(reader[index].ToString());
- }
-
- index++;
- info.Date = reader[index].ReadDateTime();
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.Severity = Enum.Parse<LogLevel>(reader[index].ToString(), true);
- }
-
- return info;
- }
- }
-}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index ffc916b98..e6410f857 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -22,7 +22,6 @@ using Emby.Dlna.Ssdp;
using Emby.Drawing;
using Emby.Notifications;
using Emby.Photos;
-using Emby.Server.Implementations.Activity;
using Emby.Server.Implementations.Archiving;
using Emby.Server.Implementations.Channels;
using Emby.Server.Implementations.Collections;
@@ -44,7 +43,6 @@ using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
-using Emby.Server.Implementations.SocketSharp;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using MediaBrowser.Api;
@@ -82,7 +80,6 @@ using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
-using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
@@ -94,7 +91,6 @@ using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
@@ -102,11 +98,10 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using Prometheus.DotNetRuntime;
+using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations
{
@@ -503,32 +498,8 @@ namespace Emby.Server.Implementations
RegisterServices(serviceCollection);
}
- public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
- {
- 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);
- }
+ public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
+ => _httpServer.RequestHandler(context);
/// <summary>
/// Registers services/resources with the service collection that will be available via DI.
@@ -546,13 +517,6 @@ namespace Emby.Server.Implementations
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;
- });
-
serviceCollection.AddSingleton(_fileSystemManager);
serviceCollection.AddSingleton<TvdbClientManager>();
@@ -621,7 +585,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<ServiceController>();
- serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
@@ -663,9 +626,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
- serviceCollection.AddSingleton<IActivityRepository, ActivityRepository>();
- serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
-
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
serviceCollection.AddSingleton<ISessionContext, SessionContext>();
@@ -696,7 +656,6 @@ namespace Emby.Server.Implementations
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
- ((ActivityRepository)Resolve<IActivityRepository>()).Initialize();
SetStaticProperties();
@@ -1150,9 +1109,6 @@ namespace Emby.Server.Implementations
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
- HttpServerPortNumber = HttpPort,
- SupportsHttps = SupportsHttps,
- HttpsPortNumber = HttpsPort,
OperatingSystem = OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = OperatingSystem.Name,
CanSelfRestart = CanSelfRestart,
@@ -1188,23 +1144,22 @@ namespace Emby.Server.Implementations
};
}
- public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps;
-
- public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy;
+ /// <inheritdoc/>
+ public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
- public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false)
+ /// <inheritdoc/>
+ public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken)
{
try
{
// Return the first matched address, if found, or the first known local address
var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
-
- foreach (var address in addresses)
+ if (addresses.Count == 0)
{
- return GetLocalApiUrl(address, forceHttp);
+ return null;
}
- return null;
+ return GetLocalApiUrl(addresses.First());
}
catch (Exception ex)
{
@@ -1231,7 +1186,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc />
- public string GetLocalApiUrl(IPAddress ipAddress, bool forceHttp = false)
+ public string GetLocalApiUrl(IPAddress ipAddress)
{
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
@@ -1241,29 +1196,30 @@ namespace Emby.Server.Implementations
str.CopyTo(span.Slice(1));
span[^1] = ']';
- return GetLocalApiUrl(span, forceHttp);
+ return GetLocalApiUrl(span);
}
- return GetLocalApiUrl(ipAddress.ToString(), forceHttp);
+ return GetLocalApiUrl(ipAddress.ToString());
}
- /// <inheritdoc />
- public string GetLocalApiUrl(ReadOnlySpan<char> host, bool forceHttp = false)
+ /// <inheritdoc/>
+ public string GetLoopbackHttpApiUrl()
{
- var url = new StringBuilder(64);
- bool useHttps = EnableHttps && !forceHttp;
- url.Append(useHttps ? "https://" : "http://")
- .Append(host)
- .Append(':')
- .Append(useHttps ? HttpsPort : HttpPort);
-
- string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
- if (baseUrl.Length != 0)
- {
- url.Append(baseUrl);
- }
+ return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
+ }
- return url.ToString();
+ /// <inheritdoc/>
+ 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
+ {
+ Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
+ Host = host.ToString(),
+ Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
+ Path = ServerConfigurationManager.Configuration.BaseUrl
+ }.ToString().TrimEnd('/');
}
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
@@ -1297,7 +1253,7 @@ namespace Emby.Server.Implementations
}
}
- var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
+ var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
if (valid)
{
resultList.Add(address);
@@ -1331,7 +1287,7 @@ namespace Emby.Server.Implementations
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)
|| address.Equals(IPAddress.IPv6Loopback))
@@ -1339,8 +1295,7 @@ namespace Emby.Server.Implementations
return true;
}
- var apiUrl = GetLocalApiUrl(address);
- apiUrl += "/system/ping";
+ var apiUrl = GetLocalApiUrl(address) + "/system/ping";
if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
{
diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
index 96096e142..7f7c6a0be 100644
--- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs
+++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
@@ -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.
/// </summary>
/// <param name="appHost">The application host.</param>
- /// <param name="url">The URL.</param>
- private static void TryOpenUrl(IServerApplicationHost appHost, string url)
+ /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param>
+ private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl)
{
try
{
string baseUrl = appHost.GetLocalApiUrl("localhost");
- appHost.LaunchUrl(baseUrl + url);
+ appHost.LaunchUrl(baseUrl + relativeUrl);
}
catch (Exception ex)
{
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);
}
}
}
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index a6eaf2d0a..305e67e8c 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration
changed = true;
}
- if (!config.CameraUploadUpgraded)
- {
- config.CameraUploadUpgraded = true;
- changed = true;
- }
-
if (!config.CollectionsUpgraded)
{
config.CollectionsUpgraded = true;
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index 579cb895e..2283f2433 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -5,27 +5,18 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Devices
{
@@ -33,38 +24,23 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IJsonSerializer _json;
private readonly IUserManager _userManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILibraryMonitor _libraryMonitor;
private readonly IServerConfigurationManager _config;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localizationManager;
private readonly IAuthenticationRepository _authRepo;
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
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();
public DeviceManager(
IAuthenticationRepository authRepo,
IJsonSerializer json,
- ILibraryManager libraryManager,
- ILocalizationManager localizationManager,
IUserManager userManager,
- IFileSystem fileSystem,
- ILibraryMonitor libraryMonitor,
IServerConfigurationManager config)
{
_json = json;
_userManager = userManager;
- _fileSystem = fileSystem;
- _libraryMonitor = libraryMonitor;
_config = config;
- _libraryManager = libraryManager;
- _localizationManager = localizationManager;
_authRepo = authRepo;
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
}
@@ -194,172 +170,6 @@ namespace Emby.Server.Implementations.Devices
return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
}
- public ContentUploadHistory GetCameraUploadHistory(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(User user, string deviceId)
{
if (user == null)
@@ -399,102 +209,4 @@ namespace Emby.Server.Implementations.Devices
return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase);
}
}
-
- 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");
- }
- }
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 44fc932e3..896e4310e 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 37d7fd479..878cee23c 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
.Append(config.PublicHttpsPort).Append(Separator)
.Append(_appHost.HttpPort).Append(Separator)
.Append(_appHost.HttpsPort).Append(Separator)
- .Append(_appHost.EnableHttps).Append(Separator)
+ .Append(_appHost.ListenWithHttps).Append(Separator)
.Append(config.EnableRemoteAccess).Append(Separator)
.ToString();
}
@@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
- if (_appHost.EnableHttps)
+ if (_appHost.ListenWithHttps)
{
yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
}
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 211a0c1d9..794d55c04 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -6,11 +6,12 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
+using System.Net.WebSockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Services;
+using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -22,15 +23,17 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
using ServiceStack.Text.Jsv;
namespace Emby.Server.Implementations.HttpServer
{
- public class HttpListenerHost : IHttpServer, IDisposable
+ public class HttpListenerHost : IHttpServer
{
/// <summary>
/// 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";
private readonly ILogger _logger;
+ private readonly ILoggerFactory _loggerFactory;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
private readonly IServerApplicationHost _appHost;
private readonly IJsonSerializer _jsonSerializer;
private readonly IXmlSerializer _xmlSerializer;
- private readonly IHttpListener _socketListener;
private readonly Func<Type, Func<string, object>> _funcParseFn;
private readonly string _defaultRedirectPath;
private readonly string _baseUrlPrefix;
+
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
- private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
private readonly IHostEnvironment _hostEnvironment;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
@@ -63,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer
INetworkManager networkManager,
IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer,
- IHttpListener socketListener,
ILocalizationManager localizationManager,
ServiceController serviceController,
- IHostEnvironment hostEnvironment)
+ IHostEnvironment hostEnvironment,
+ ILoggerFactory loggerFactory)
{
_appHost = applicationHost;
_logger = logger;
@@ -76,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer
_networkManager = networkManager;
_jsonSerializer = jsonSerializer;
_xmlSerializer = xmlSerializer;
- _socketListener = socketListener;
ServiceController = serviceController;
-
- _socketListener.WebSocketConnected = OnWebSocketConnected;
_hostEnvironment = hostEnvironment;
+ _loggerFactory = loggerFactory;
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
@@ -172,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer
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)
{
if (ex is AggregateException agg)
@@ -289,32 +258,6 @@ namespace Emby.Server.Implementations.HttpServer
.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)
{
var uri = new Uri(url);
@@ -424,33 +367,52 @@ namespace Emby.Server.Implementations.HttpServer
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)
{
- 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
- if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
- || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return true;
- }
+ return true;
+ }
- if (!_networkManager.IsInLocalNetwork(remoteIp))
- {
- return false;
- }
+ if (!_networkManager.IsInLocalNetwork(remoteIp))
+ {
+ return false;
}
}
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>
/// Overridable method that can be used to implement a custom handler.
/// </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();
stopWatch.Start();
@@ -493,9 +455,10 @@ namespace Emby.Server.Implementations.HttpServer
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
httpRes.StatusCode = 200;
- httpRes.Headers.Add("Access-Control-Allow-Origin", "*");
- 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");
+ foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
+ {
+ httpRes.Headers.Add(key, value);
+ }
httpRes.ContentType = "text/plain";
await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
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
public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
{
@@ -624,7 +649,7 @@ namespace Emby.Server.Implementations.HttpServer
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);
}
- public Task ProcessWebSocketRequest(HttpContext context)
- {
- return _socketListener.ProcessWebSocketRequest(context);
- }
-
private string NormalizeEmbyRoutePath(string path)
{
_logger.LogDebug("Normalizing /emby route");
@@ -708,28 +728,6 @@ namespace Emby.Server.Implementations.HttpServer
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>
/// Processes the web socket message received.
/// </summary>
@@ -741,8 +739,6 @@ namespace Emby.Server.Implementations.HttpServer
return Task.CompletedTask;
}
- _logger.LogDebug("Websocket message received: {0}", result.MessageType);
-
IEnumerable<Task> GetTasks()
{
foreach (var x in _webSocketListeners)
diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs
deleted file mode 100644
index 501593725..000000000
--- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs
+++ /dev/null
@@ -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);
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
index 4089aa578..85c3db9b2 100644
--- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
+++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Text;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -13,14 +15,17 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary>
public class ResponseFilter
{
+ private readonly IHttpServer _server;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ResponseFilter"/> class.
/// </summary>
+ /// <param name="server">The HTTP server.</param>
/// <param name="logger">The logger.</param>
- public ResponseFilter(ILogger logger)
+ public ResponseFilter(IHttpServer server, ILogger logger)
{
+ _server = server;
_logger = logger;
}
@@ -32,10 +37,16 @@ namespace Emby.Server.Implementations.HttpServer
/// <param name="dto">The dto.</param>
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
- 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.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
- res.Headers.Add("Access-Control-Allow-Origin", "*");
+ res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
+ "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
+ "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)
{
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 2292d86a4..095725c50 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -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.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-using UtfUnknown;
namespace Emby.Server.Implementations.HttpServer
{
@@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer
private readonly ILogger _logger;
/// <summary>
- /// The json serializer.
+ /// The json serializer options.
/// </summary>
- private readonly IJsonSerializer _jsonSerializer;
+ private readonly JsonSerializerOptions _jsonOptions;
/// <summary>
/// The socket.
/// </summary>
- private readonly IWebSocket _socket;
+ private readonly WebSocket _socket;
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="socket">The socket.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
- /// <param name="logger">The logger.</param>
- /// <exception cref="ArgumentNullException">socket</exception>
- public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
+ /// <param name="query">The query.</param>
+ public WebSocketConnection(
+ ILogger<WebSocketConnection> 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;
+ _logger = logger;
_socket = socket;
- _socket.OnReceiveBytes = OnReceiveInternal;
-
RemoteEndPoint = remoteEndPoint;
- _logger = logger;
+ QueryString = query;
- socket.Closed += OnSocketClosed;
+ _jsonOptions = JsonDefaults.GetOptions();
+ LastActivityDate = DateTime.Now;
}
/// <inheritdoc />
- public event EventHandler<EventArgs> Closed;
+ public event EventHandler<EventArgs>? Closed;
/// <summary>
/// Gets or sets the remote end point.
/// </summary>
- public string RemoteEndPoint { get; private set; }
+ public IPAddress? RemoteEndPoint { get; }
/// <summary>
/// Gets or sets the receive action.
/// </summary>
/// <value>The receive action.</value>
- public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+ public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
/// <summary>
/// Gets the last activity date.
@@ -95,22 +79,10 @@ namespace Emby.Server.Implementations.HttpServer
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>
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
- public IQueryCollection QueryString { get; set; }
+ public IQueryCollection QueryString { get; }
/// <summary>
/// Gets the state.
@@ -118,138 +90,135 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The state.</value>
public WebSocketState State => _socket.State;
- void OnSocketClosed(object sender, EventArgs e)
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="message">The message.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
{
- Closed?.Invoke(this, EventArgs.Empty);
+ var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+ return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
}
- /// <summary>
- /// Called when [receive].
- /// </summary>
- /// <param name="bytes">The bytes.</param>
- private void OnReceiveInternal(byte[] bytes)
+ /// <inheritdoc />
+ public async Task ProcessAsync(CancellationToken cancellationToken = default)
{
- LastActivityDate = DateTime.UtcNow;
+ var pipe = new Pipe();
+ var writer = pipe.Writer;
- if (OnReceive == null)
+ ValueWebSocketReceiveResult receiveresult;
+ do
{
- return;
- }
- var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
+ // 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;
+ }
- if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
- {
- OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
- }
- else
+ 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)
{
- OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
+ await _socket.CloseAsync(
+ WebSocketCloseStatus.NormalClosure,
+ string.Empty,
+ cancellationToken).ConfigureAwait(false);
}
}
- private void OnReceiveInternal(string message)
+ private async Task ProcessInternal(PipeReader reader)
{
- 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;
- }
+ ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
+ ReadOnlySequence<byte> buffer = result.Buffer;
if (OnReceive == null)
{
+ // Tell the PipeReader how much of the buffer we have consumed
+ reader.AdvanceTo(buffer.End);
return;
}
+ WebSocketMessage<object> stub;
try
{
- var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
- var info = new WebSocketMessageInfo
+ if (buffer.IsSingleSegment)
{
- MessageType = stub.MessageType,
- Data = stub.Data?.ToString(),
- Connection = this
- };
-
- OnReceive(info);
+ 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 (Exception ex)
+ 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");
- }
- }
-
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="message">The message.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">message</exception>
- public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
- {
- if (message == null)
- {
- 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));
+ return;
}
- cancellationToken.ThrowIfCancellationRequested();
+ // Tell the PipeReader how much of the buffer we have consumed
+ reader.AdvanceTo(buffer.End);
- return _socket.SendAsync(buffer, true, cancellationToken);
- }
+ _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
- /// <inheritdoc />
- public Task SendAsync(string text, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(text))
+ var info = new WebSocketMessageInfo
{
- throw new ArgumentNullException(nameof(text));
- }
+ MessageType = stub.MessageType,
+ Data = stub.Data?.ToString(), // Data can be null
+ Connection = this
+ };
- cancellationToken.ThrowIfCancellationRequested();
-
- return _socket.SendAsync(text, true, cancellationToken);
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- _socket.Dispose();
- }
+ await OnReceive(info).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 85b1b6e32..6c9ba7c27 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary>
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
{
- private readonly ILogger _logger;
+ private readonly ILogger<MusicAlbumResolver> _logger;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</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;
_fileSystem = fileSystem;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 681db4896..5f5cd0e92 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary>
public class MusicArtistResolver : ItemResolver<MusicArtist>
{
- private readonly ILogger _logger;
+ private readonly ILogger<MusicAlbumResolver> _logger;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
@@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </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="libraryManager">The library manager.</param>
/// <param name="config">The configuration manager.</param>
public MusicArtistResolver(
- ILogger<MusicArtistResolver> logger,
+ ILogger<MusicAlbumResolver> logger,
IFileSystem fileSystem,
ILibraryManager libraryManager,
IServerConfigurationManager config)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 0b93ebeb8..503de0b4e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
{
- private readonly string[] _validExtensions = { ".pdf", ".epub", ".mobi", ".cbr", ".cbz", ".azw3" };
+ private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 33f4ca146..3efe1ee25 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
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,
Path = info.Path,
Protocol = MediaProtocol.File,
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index d89a816b3..82b1f3cf1 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
//OpenedMediaSource.Path = tempFile;
//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;
//OpenedMediaSource.SupportsDirectPlay = false;
//OpenedMediaSource.SupportsDirectStream = true;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index f5dda79db..f7c9c736e 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public M3UTunerHost(
IServerConfigurationManager config,
IMediaSourceManager mediaSourceManager,
- ILogger logger,
+ ILogger<M3UTunerHost> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IHttpClient httpClient,
@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- private static readonly string[] _disallowedSharedStreamExtensions = new string[]
+ private static readonly string[] _disallowedSharedStreamExtensions =
{
".mkv",
".mp4",
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 0e600202a..083fcd029 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
//OpenedMediaSource.Path = tempFile;
//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;
//OpenedMediaSource.Path = TempFilePath;
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 50d0d083c..5637ce346 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -97,5 +97,9 @@
"TasksApplicationCategory": "Applikasjon",
"TasksLibraryCategory": "Bibliotek",
"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."
}
diff --git a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
deleted file mode 100644
index fda32da5e..000000000
--- a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
+++ /dev/null
@@ -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);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Net/IWebSocket.cs b/Emby.Server.Implementations/Net/IWebSocket.cs
deleted file mode 100644
index 4d160aa66..000000000
--- a/Emby.Server.Implementations/Net/IWebSocket.cs
+++ /dev/null
@@ -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);
- }
-}
diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
deleted file mode 100644
index 6880766f9..000000000
--- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs
deleted file mode 100644
index dfb81816c..000000000
--- a/Emby.Server.Implementations/Session/HttpSessionController.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index c93c02c48..df98a35bc 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -477,8 +477,7 @@ namespace Emby.Server.Implementations.Session
Client = appName,
DeviceId = deviceId,
ApplicationVersion = appVersion,
- Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
- ServerId = _appHost.SystemId
+ Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
var username = user?.Name;
@@ -1042,12 +1041,12 @@ namespace Emby.Server.Implementations.Session
private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
{
- var controllers = session.SessionControllers.ToArray();
- var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ var controllers = session.SessionControllers;
+ var messageId = Guid.NewGuid();
foreach (var controller in controllers)
{
- await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false);
+ await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
}
}
@@ -1055,13 +1054,13 @@ namespace Emby.Server.Implementations.Session
{
IEnumerable<Task> GetTasks()
{
- var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ var messageId = Guid.NewGuid();
foreach (var session in sessions)
{
var controllers = session.SessionControllers;
foreach (var controller in controllers)
{
- yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken);
+ yield return controller.SendMessage(name, messageId, data, cancellationToken);
}
}
}
@@ -1762,7 +1761,7 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(info));
}
- var user = info.UserId.Equals(Guid.Empty)
+ var user = info.UserId == Guid.Empty
? null
: _userManager.GetUserById(info.UserId);
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 930f2d35d..d4e4ba1f2 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -3,7 +3,6 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -12,7 +11,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Class SessionWebSocketListener
/// </summary>
- public class SessionWebSocketListener : IWebSocketListener, IDisposable
+ public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
{
/// <summary>
/// The _session manager
@@ -23,42 +22,41 @@ namespace Emby.Server.Implementations.Session
/// The _logger
/// </summary>
private readonly ILogger _logger;
-
- /// <summary>
- /// The _dto service
- /// </summary>
- private readonly IJsonSerializer _json;
+ private readonly ILoggerFactory _loggerFactory;
private readonly IHttpServer _httpServer;
-
/// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
- /// <param name="json">The json.</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;
- _logger = loggerFactory.CreateLogger(GetType().Name);
- _json = json;
+ _loggerFactory = loggerFactory;
_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)
{
EnsureController(session, e.Argument);
}
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);
}
+ /// <inheritdoc />
public void Dispose()
{
- _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected;
+ _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
}
/// <summary>
@@ -94,7 +93,8 @@ namespace Emby.Server.Implementations.Session
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;
controller.AddWebSocket(connection);
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index 0d483c55f..a0274acd2 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Linq;
@@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging;
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 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;
+ _session = session;
_sessionManager = sessionManager;
- Sockets = new List<IWebSocketConnection>();
+ _sockets = new List<IWebSocketConnection>();
}
private bool HasOpenSockets => GetActiveSockets().Any();
+ /// <inheritdoc />
public bool SupportsMediaControl => HasOpenSockets;
+ /// <inheritdoc />
public bool IsSessionActive => HasOpenSockets;
private IEnumerable<IWebSocketConnection> GetActiveSockets()
- {
- return Sockets
- .OrderByDescending(i => i.LastActivityDate)
- .Where(i => i.State == WebSocketState.Open);
- }
+ => _sockets.Where(i => i.State == WebSocketState.Open);
public void AddWebSocket(IWebSocketConnection connection)
{
- var sockets = Sockets.ToList();
- sockets.Add(connection);
+ _logger.LogDebug("Adding websocket to session {Session}", _session.Id);
+ _sockets.Add(connection);
- Sockets = sockets;
-
- connection.Closed += connection_Closed;
+ connection.Closed += OnConnectionClosed;
}
- void connection_Closed(object sender, EventArgs e)
+ private void OnConnectionClosed(object sender, EventArgs e)
{
var connection = (IWebSocketConnection)sender;
- var sockets = Sockets.ToList();
- sockets.Remove(connection);
-
- Sockets = sockets;
-
- _sessionManager.CloseIfNeeded(Session);
+ _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
+ _sockets.Remove(connection);
+ connection.Closed -= OnConnectionClosed;
+ _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()
+ .OrderByDescending(i => i.LastActivityDate)
.FirstOrDefault();
if (socket == null)
@@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session
return Task.CompletedTask;
}
- return socket.SendAsync(new WebSocketMessage<T>
- {
- Data = data,
- MessageType = name,
- MessageId = messageId
-
- }, cancellationToken);
+ return socket.SendAsync(
+ new WebSocketMessage<T>
+ {
+ Data = data,
+ MessageType = name,
+ MessageId = messageId
+ },
+ cancellationToken);
}
+ /// <inheritdoc />
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;
}
}
}
diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
deleted file mode 100644
index 67521d6c6..000000000
--- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
deleted file mode 100644
index b85750c9b..000000000
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
deleted file mode 100644
index eb1877440..000000000
--- a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
+++ /dev/null
@@ -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);
- }
-}
diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs
deleted file mode 100644
index 31a7468fb..000000000
--- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs
+++ /dev/null
@@ -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");
- }
- }
- }
-}
diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs
new file mode 100644
index 000000000..522c20664
--- /dev/null
+++ b/Jellyfin.Data/Entities/ActivityLog.cs
@@ -0,0 +1,154 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Data.Entities
+{
+ /// <summary>
+ /// An entity referencing an activity log entry.
+ /// </summary>
+ public partial class ActivityLog : ISavingChanges
+ {
+ /// <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>
+ /// Initializes a new instance of the <see cref="ActivityLog"/> class.
+ /// Default constructor. Protected due to required properties, but present because EF needs it.
+ /// </summary>
+ protected ActivityLog()
+ {
+ Init();
+ }
+
+ /// <summary>
+ /// Static create function (for use in LINQ queries, etc.)
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="userId">The user's id.</param>
+ /// <returns>The new <see cref="ActivityLog"/> instance.</returns>
+ public static ActivityLog Create(string name, string type, Guid userId)
+ {
+ return new ActivityLog(name, type, userId);
+ }
+
+ /*************************************************************************
+ * Properties
+ *************************************************************************/
+
+ /// <summary>
+ /// Gets or sets the identity of this instance.
+ /// This is the key in the backing database.
+ /// </summary>
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; protected set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// Required, Max length = 512.
+ /// </summary>
+ [Required]
+ [MaxLength(512)]
+ [StringLength(512)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the overview.
+ /// Max length = 512.
+ /// </summary>
+ [MaxLength(512)]
+ [StringLength(512)]
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the short overview.
+ /// Max length = 512.
+ /// </summary>
+ [MaxLength(512)]
+ [StringLength(512)]
+ public string ShortOverview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// Required, Max length = 256.
+ /// </summary>
+ [Required]
+ [MaxLength(256)]
+ [StringLength(256)]
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// Required.
+ /// </summary>
+ [Required]
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item id.
+ /// Max length = 256.
+ /// </summary>
+ [MaxLength(256)]
+ [StringLength(256)]
+ public string ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created. This should be in UTC.
+ /// Required.
+ /// </summary>
+ [Required]
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>.
+ /// Required.
+ /// </summary>
+ [Required]
+ public LogLevel LogSeverity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the row version.
+ /// Required, ConcurrencyToken.
+ /// </summary>
+ [ConcurrencyCheck]
+ [Required]
+ public uint RowVersion { get; set; }
+
+ partial void Init();
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 73ea593b0..b2a3f7eb3 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,12 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>netstandard2.0</TargetFramework>
+ <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <!-- Code analysers-->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.2.4" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.3" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
</ItemGroup>
</Project>
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
new file mode 100644
index 000000000..65ceee32b
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Querying;
+
+namespace Jellyfin.Server.Implementations.Activity
+{
+ /// <summary>
+ /// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
+ /// </summary>
+ public class ActivityManager : IActivityManager
+ {
+ private readonly JellyfinDbProvider _provider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ActivityManager"/> class.
+ /// </summary>
+ /// <param name="provider">The Jellyfin database provider.</param>
+ public ActivityManager(JellyfinDbProvider provider)
+ {
+ _provider = provider;
+ }
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
+
+ /// <inheritdoc/>
+ public void Create(ActivityLog entry)
+ {
+ using var dbContext = _provider.CreateContext();
+ dbContext.ActivityLogs.Add(entry);
+ dbContext.SaveChanges();
+
+ EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
+ }
+
+ /// <inheritdoc/>
+ public async Task CreateAsync(ActivityLog entry)
+ {
+ using var dbContext = _provider.CreateContext();
+ await dbContext.ActivityLogs.AddAsync(entry);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+
+ EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
+ }
+
+ /// <inheritdoc/>
+ public QueryResult<ActivityLogEntry> GetPagedResult(
+ Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
+ int? startIndex,
+ int? limit)
+ {
+ using var dbContext = _provider.CreateContext();
+
+ var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated));
+
+ if (startIndex.HasValue)
+ {
+ query = query.Skip(startIndex.Value);
+ }
+
+ if (limit.HasValue)
+ {
+ query = query.Take(limit.Value);
+ }
+
+ // This converts the objects from the new database model to the old for compatibility with the existing API.
+ var list = query.Select(ConvertToOldModel).ToList();
+
+ return new QueryResult<ActivityLogEntry>
+ {
+ Items = list,
+ TotalRecordCount = func(dbContext.ActivityLogs).Count()
+ };
+ }
+
+ /// <inheritdoc/>
+ public QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit)
+ {
+ return GetPagedResult(logs => logs, startIndex, limit);
+ }
+
+ private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
+ {
+ return new ActivityLogEntry
+ {
+ Id = entry.Id,
+ Name = entry.Name,
+ Overview = entry.Overview,
+ ShortOverview = entry.ShortOverview,
+ Type = entry.Type,
+ ItemId = entry.ItemId,
+ UserId = entry.UserId,
+ Date = entry.DateCreated,
+ Severity = entry.LogSeverity
+ };
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index a31f28f64..149ca5020 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -26,6 +26,14 @@
</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>
<ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index 76343edf9..ec09a619f 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -15,6 +15,7 @@ namespace Jellyfin.Server.Implementations
/// <inheritdoc/>
public partial class JellyfinDb : DbContext
{
+ public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
/*public virtual DbSet<Artwork> Artwork { get; set; }
public virtual DbSet<Book> Books { get; set; }
public virtual DbSet<BookMetadata> BookMetadata { get; set; }
@@ -49,6 +50,7 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<Preference> Preferences { get; set; }
public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
public virtual DbSet<Rating> Ratings { get; set; }
+
/// <summary>
/// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
/// store review ratings, not age ratings
@@ -93,8 +95,10 @@ namespace Jellyfin.Server.Implementations
modelBuilder.HasDefaultSchema("jellyfin");
/*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind);
+
modelBuilder.Entity<Genre>().HasIndex(t => t.Name)
.IsUnique();
+
modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId)
.IsUnique();*/
@@ -103,9 +107,10 @@ namespace Jellyfin.Server.Implementations
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();
}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
new file mode 100644
index 000000000..eab531d38
--- /dev/null
+++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
@@ -0,0 +1,33 @@
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Jellyfin.Server.Implementations
+{
+ /// <summary>
+ /// Factory class for generating new <see cref="JellyfinDb"/> instances.
+ /// </summary>
+ public class JellyfinDbProvider
+ {
+ private readonly IServiceProvider _serviceProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
+ /// </summary>
+ /// <param name="serviceProvider">The application's service provider.</param>
+ public JellyfinDbProvider(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ serviceProvider.GetService<JellyfinDb>().Database.Migrate();
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="JellyfinDb"/> context.
+ /// </summary>
+ /// <returns>The newly created context.</returns>
+ public JellyfinDb CreateContext()
+ {
+ return _serviceProvider.GetRequiredService<JellyfinDb>();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
new file mode 100644
index 000000000..98a83b745
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
@@ -0,0 +1,72 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDb))]
+ [Migration("20200514181226_AddActivityLog")]
+ partial class AddActivityLog
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "3.1.3");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("ActivityLogs");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs
new file mode 100644
index 000000000..5e0b454d8
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs
@@ -0,0 +1,46 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class AddActivityLog : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.EnsureSchema(
+ name: "jellyfin");
+
+ migrationBuilder.CreateTable(
+ name: "ActivityLogs",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Name = table.Column<string>(maxLength: 512, nullable: false),
+ Overview = table.Column<string>(maxLength: 512, nullable: true),
+ ShortOverview = table.Column<string>(maxLength: 512, nullable: true),
+ Type = table.Column<string>(maxLength: 256, nullable: false),
+ UserId = table.Column<Guid>(nullable: false),
+ ItemId = table.Column<string>(maxLength: 256, nullable: true),
+ DateCreated = table.Column<DateTime>(nullable: false),
+ LogSeverity = table.Column<int>(nullable: false),
+ RowVersion = table.Column<uint>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ActivityLogs", x => x.Id);
+ });
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "ActivityLogs",
+ schema: "jellyfin");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
new file mode 100644
index 000000000..72a4a8c3b
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
@@ -0,0 +1,20 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <summary>
+ /// The design time factory for <see cref="JellyfinDb"/>.
+ /// This is only used for the creation of migrations and not during runtime.
+ /// </summary>
+ internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory<JellyfinDb>
+ {
+ public JellyfinDb CreateDbContext(string[] args)
+ {
+ var optionsBuilder = new DbContextOptionsBuilder<JellyfinDb>();
+ optionsBuilder.UseSqlite("Data Source=jellyfin.db");
+
+ return new JellyfinDb(optionsBuilder.Options);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
new file mode 100644
index 000000000..1e7ffd235
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -0,0 +1,66 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDb))]
+ partial class JellyfinDbModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "3.1.3");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("ActivityLogs");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index f678e714c..331a32c73 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -1,12 +1,17 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Reflection;
using Emby.Drawing;
using Emby.Server.Implementations;
using Jellyfin.Drawing.Skia;
+using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Activity;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -56,6 +61,15 @@ namespace Jellyfin.Server
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
}
+ // TODO: Set up scoping and use AddDbContextPool
+ serviceCollection.AddDbContext<JellyfinDb>(
+ options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
+ ServiceLifetime.Transient);
+
+ serviceCollection.AddSingleton<JellyfinDbProvider>();
+
+ serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
+
base.RegisterServices(serviceCollection);
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 88114d999..9eec6ed4e 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -60,6 +60,7 @@
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
<ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
+ <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
</ItemGroup>
</Project>
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index ca1748282..473f62737 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -17,7 +17,9 @@ namespace Jellyfin.Server.Migrations
private static readonly Type[] _migrationTypes =
{
typeof(Routines.DisableTranscodingThrottling),
- typeof(Routines.CreateUserLoggingConfigFile)
+ typeof(Routines.CreateUserLoggingConfigFile),
+ typeof(Routines.MigrateActivityLogDb),
+ typeof(Routines.RemoveDuplicateExtras)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
new file mode 100644
index 000000000..b3cc29708
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Emby.Server.Implementations.Data;
+using Jellyfin.Data.Entities;
+using Jellyfin.Server.Implementations;
+using MediaBrowser.Controller;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// The migration routine for migrating the activity log database to EF Core.
+ /// </summary>
+ public class MigrateActivityLogDb : IMigrationRoutine
+ {
+ private const string DbFilename = "activitylog.db";
+
+ private readonly ILogger<MigrateActivityLogDb> _logger;
+ private readonly JellyfinDbProvider _provider;
+ 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)
+ {
+ _logger = logger;
+ _provider = provider;
+ _paths = paths;
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978");
+
+ /// <inheritdoc/>
+ public string Name => "MigrateActivityLogDatabase";
+
+ /// <inheritdoc/>
+ 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;
+ using (var connection = SQLite3.Open(
+ Path.Combine(dataPath, DbFilename),
+ ConnectionFlags.ReadOnly,
+ null))
+ {
+ _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
+ using var dbContext = _provider.CreateContext();
+
+ var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC");
+
+ // Make sure that the database is empty in case of failed migration due to power outages, etc.
+ dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs);
+ dbContext.SaveChanges();
+ // Reset the autoincrement counter
+ dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';");
+ dbContext.SaveChanges();
+
+ var newEntries = queryResult.Select(entry =>
+ {
+ if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity))
+ {
+ severity = LogLevel.Trace;
+ }
+
+ var newEntry = new ActivityLog(
+ entry[1].ToString(),
+ entry[4].ToString(),
+ entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString()))
+ {
+ DateCreated = entry[7].ReadDateTime(),
+ LogSeverity = severity
+ };
+
+ if (entry[2].SQLiteType != SQLiteType.Null)
+ {
+ newEntry.Overview = entry[2].ToString();
+ }
+
+ if (entry[3].SQLiteType != SQLiteType.Null)
+ {
+ newEntry.ShortOverview = entry[3].ToString();
+ }
+
+ if (entry[5].SQLiteType != SQLiteType.Null)
+ {
+ newEntry.ItemId = entry[5].ToString();
+ }
+
+ return newEntry;
+ });
+
+ dbContext.ActivityLogs.AddRange(newEntries);
+ dbContext.SaveChanges();
+ }
+
+ try
+ {
+ 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)
+ {
+ _logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'");
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
new file mode 100644
index 000000000..e95536388
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
@@ -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')");
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index ae423532e..b9895386f 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -10,14 +10,11 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
-using Emby.Drawing;
using Emby.Server.Implementations;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Networking;
-using Jellyfin.Drawing.Skia;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.WebDashboard.Api;
using Microsoft.AspNetCore.Hosting;
@@ -297,7 +294,7 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Kestrel listening on {IpAddress}", address);
options.Listen(address, appHost.HttpPort);
- if (appHost.EnableHttps && appHost.Certificate != null)
+ if (appHost.ListenWithHttps)
{
options.Listen(address, appHost.HttpsPort, listenOptions =>
{
@@ -327,7 +324,7 @@ namespace Jellyfin.Server
_logger.LogInformation("Kestrel listening on all interfaces");
options.ListenAnyIP(appHost.HttpPort);
- if (appHost.EnableHttps && appHost.Certificate != null)
+ if (appHost.ListenWithHttps)
{
options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
{
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 8bcfd1350..5f9a5c161 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -64,7 +64,6 @@ namespace Jellyfin.Server
app.UseResponseCompression();
// TODO app.UseMiddleware<WebSocketMiddleware>();
- app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
// TODO use when old API is removed: app.UseAuthentication();
app.UseJellyfinApiSwagger();
diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs
index 1a1d86362..2cd68ac1b 100644
--- a/MediaBrowser.Api/BaseApiService.cs
+++ b/MediaBrowser.Api/BaseApiService.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Api
public abstract class BaseApiService : IService, IRequiresRequest
{
public BaseApiService(
- ILogger logger,
+ ILogger<BaseApiService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory)
{
@@ -34,7 +34,7 @@ namespace MediaBrowser.Api
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
- protected ILogger Logger { get; }
+ protected ILogger<BaseApiService> Logger { get; }
/// <summary>
/// Gets or sets the server configuration manager.
diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs
index 7004a2559..53eb9667d 100644
--- a/MediaBrowser.Api/Devices/DeviceService.cs
+++ b/MediaBrowser.Api/Devices/DeviceService.cs
@@ -1,5 +1,4 @@
using System.IO;
-using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Net;
@@ -116,11 +115,6 @@ namespace MediaBrowser.Api.Devices
return _deviceManager.GetDeviceOptions(request.Id);
}
- public object Get(GetCameraUploads request)
- {
- return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId));
- }
-
public void Delete(DeleteDevice request)
{
var sessions = _authRepo.Get(new AuthenticationInfoQuery
@@ -134,35 +128,5 @@ namespace MediaBrowser.Api.Devices
_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
- });
- }
}
}
diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs
index a54640b2f..c0146dfee 100644
--- a/MediaBrowser.Api/Library/LibraryService.cs
+++ b/MediaBrowser.Api/Library/LibraryService.cs
@@ -319,11 +319,14 @@ namespace MediaBrowser.Api.Library
private readonly ILocalizationManager _localization;
private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILogger<MoviesService> _moviesServiceLogger;
+
/// <summary>
/// Initializes a new instance of the <see cref="LibraryService" /> class.
/// </summary>
public LibraryService(
ILogger<LibraryService> logger,
+ ILogger<MoviesService> moviesServiceLogger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IProviderManager providerManager,
@@ -344,6 +347,7 @@ namespace MediaBrowser.Api.Library
_activityManager = activityManager;
_localization = localization;
_libraryMonitor = libraryMonitor;
+ _moviesServiceLogger = moviesServiceLogger;
}
private string[] GetRepresentativeItemTypes(string contentType)
@@ -543,7 +547,7 @@ namespace MediaBrowser.Api.Library
if (item is Movie || (program != null && program.IsMovie) || item is Trailer)
{
return new MoviesService(
- Logger,
+ _moviesServiceLogger,
ServerConfigurationManager,
ResultFactory,
_userManager,
@@ -759,13 +763,12 @@ namespace MediaBrowser.Api.Library
{
try
{
- _activityManager.Create(new ActivityLogEntry
+ _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog(
+ string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name),
+ "UserDownloadingContent",
+ auth.UserId)
{
- Name = string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name),
- Type = "UserDownloadingContent",
ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
- UserId = auth.UserId
-
});
}
catch
diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs
index 46da8b909..cdd027634 100644
--- a/MediaBrowser.Api/Movies/MoviesService.cs
+++ b/MediaBrowser.Api/Movies/MoviesService.cs
@@ -82,7 +82,7 @@ namespace MediaBrowser.Api.Movies
/// Initializes a new instance of the <see cref="MoviesService" /> class.
/// </summary>
public MoviesService(
- ILogger logger,
+ ILogger<MoviesService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs
index 8adf9c621..0b5334235 100644
--- a/MediaBrowser.Api/Movies/TrailersService.cs
+++ b/MediaBrowser.Api/Movies/TrailersService.cs
@@ -33,13 +33,18 @@ namespace MediaBrowser.Api.Movies
/// </summary>
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 ILocalizationManager _localizationManager;
private readonly IJsonSerializer _json;
private readonly IAuthorizationContext _authContext;
public TrailersService(
- ILogger<TrailersService> logger,
+ ILoggerFactory loggerFactory,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
@@ -48,7 +53,7 @@ namespace MediaBrowser.Api.Movies
ILocalizationManager localizationManager,
IJsonSerializer json,
IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
+ : base(loggerFactory.CreateLogger<TrailersService>(), serverConfigurationManager, httpResultFactory)
{
_userManager = userManager;
_libraryManager = libraryManager;
@@ -56,6 +61,7 @@ namespace MediaBrowser.Api.Movies
_localizationManager = localizationManager;
_json = json;
_authContext = authContext;
+ _logger = loggerFactory.CreateLogger<ItemsService>();
}
public object Get(Getrailers request)
@@ -66,7 +72,7 @@ namespace MediaBrowser.Api.Movies
getItems.IncludeItemTypes = "Trailer";
return new ItemsService(
- Logger,
+ _logger,
ServerConfigurationManager,
ResultFactory,
_userManager,
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
index 928ca1612..f796aa486 100644
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -81,7 +81,7 @@ namespace MediaBrowser.Api.Playback
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
/// </summary>
protected BaseStreamingService(
- ILogger logger,
+ ILogger<BaseStreamingService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
index 4213193ba..627421aac 100644
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Api.Playback.Hls
public abstract class BaseHlsService : BaseStreamingService
{
public BaseHlsService(
- ILogger logger,
+ ILogger<BaseHlsService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index 7f74e85e9..061316cb8 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -94,7 +94,7 @@ namespace MediaBrowser.Api.Playback.Hls
public class DynamicHlsService : BaseHlsService
{
public DynamicHlsService(
- ILogger logger,
+ ILogger<DynamicHlsService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
index db24eaca6..e2d771ec6 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -79,7 +79,7 @@ namespace MediaBrowser.Api.Playback
private readonly IAuthorizationContext _authContext;
public MediaInfoService(
- ILogger logger,
+ ILogger<MediaInfoService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IMediaSourceManager mediaSourceManager,
diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
index 8d1e3a3f2..34c7986ca 100644
--- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Api.Playback.Progressive
public class AudioService : BaseProgressiveStreamingService
{
public AudioService(
- ILogger logger,
+ ILogger<AudioService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IHttpClient httpClient,
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
index ed68219c9..c7bf055fb 100644
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
@@ -28,7 +28,7 @@ namespace MediaBrowser.Api.Playback.Progressive
protected IHttpClient HttpClient { get; private set; }
public BaseProgressiveStreamingService(
- ILogger logger,
+ ILogger<BaseProgressiveStreamingService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IHttpClient httpClient,
diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs
index cebd4b49a..a3b319d44 100644
--- a/MediaBrowser.Api/Playback/UniversalAudioService.cs
+++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs
@@ -75,9 +75,11 @@ namespace MediaBrowser.Api.Playback
public class UniversalAudioService : BaseApiService
{
private readonly EncodingHelper _encodingHelper;
+ private readonly ILoggerFactory _loggerFactory;
public UniversalAudioService(
ILogger<UniversalAudioService> logger,
+ ILoggerFactory loggerFactory,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IHttpClient httpClient,
@@ -108,6 +110,7 @@ namespace MediaBrowser.Api.Playback
AuthorizationContext = authorizationContext;
NetworkManager = networkManager;
_encodingHelper = encodingHelper;
+ _loggerFactory = loggerFactory;
}
protected IHttpClient HttpClient { get; private set; }
@@ -233,7 +236,7 @@ namespace MediaBrowser.Api.Playback
AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
var mediaInfoService = new MediaInfoService(
- Logger,
+ _loggerFactory.CreateLogger<MediaInfoService>(),
ServerConfigurationManager,
ResultFactory,
MediaSourceManager,
@@ -277,7 +280,7 @@ namespace MediaBrowser.Api.Playback
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
var service = new DynamicHlsService(
- Logger,
+ _loggerFactory.CreateLogger<DynamicHlsService>(),
ServerConfigurationManager,
ResultFactory,
UserManager,
@@ -331,7 +334,7 @@ namespace MediaBrowser.Api.Playback
else
{
var service = new AudioService(
- Logger,
+ _loggerFactory.CreateLogger<AudioService>(),
ServerConfigurationManager,
ResultFactory,
HttpClient,
diff --git a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
index d882aac88..0e74c9267 100644
--- a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
+++ b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
@@ -31,46 +31,46 @@ namespace MediaBrowser.Api.Sessions
{
_sessionManager = sessionManager;
- _sessionManager.SessionStarted += _sessionManager_SessionStarted;
- _sessionManager.SessionEnded += _sessionManager_SessionEnded;
- _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
- _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
- _sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress;
- _sessionManager.CapabilitiesChanged += _sessionManager_CapabilitiesChanged;
- _sessionManager.SessionActivity += _sessionManager_SessionActivity;
+ _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity += OnSessionManagerSessionActivity;
}
- void _sessionManager_SessionActivity(object sender, SessionEventArgs e)
+ private void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
{
SendData(false);
}
- void _sessionManager_CapabilitiesChanged(object sender, SessionEventArgs e)
+ private void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e)
{
SendData(true);
}
- void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e)
{
SendData(!e.IsAutomated);
}
- void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
+ private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
SendData(true);
}
- void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
+ private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
{
SendData(true);
}
- void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
+ private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{
SendData(true);
}
- void _sessionManager_SessionStarted(object sender, SessionEventArgs e)
+ private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
{
SendData(true);
}
@@ -84,15 +84,16 @@ namespace MediaBrowser.Api.Sessions
return Task.FromResult(_sessionManager.Sessions);
}
+ /// <inheritdoc />
protected override void Dispose(bool dispose)
{
- _sessionManager.SessionStarted -= _sessionManager_SessionStarted;
- _sessionManager.SessionEnded -= _sessionManager_SessionEnded;
- _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
- _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
- _sessionManager.PlaybackProgress -= _sessionManager_PlaybackProgress;
- _sessionManager.CapabilitiesChanged -= _sessionManager_CapabilitiesChanged;
- _sessionManager.SessionActivity -= _sessionManager_SessionActivity;
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
base.Dispose(dispose);
}
diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs
index f95fa7ca0..a6bacad4f 100644
--- a/MediaBrowser.Api/System/ActivityLogService.cs
+++ b/MediaBrowser.Api/System/ActivityLogService.cs
@@ -1,5 +1,7 @@
using System;
using System.Globalization;
+using System.Linq;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
@@ -53,7 +55,10 @@ namespace MediaBrowser.Api.System
(DateTime?)null :
DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
- var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, 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);
}
diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
index f8b6ee65d..8e4860be4 100644
--- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
+++ b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
@@ -10,7 +10,7 @@ namespace MediaBrowser.Api.System
/// <summary>
/// Class SessionInfoWebSocketListener
/// </summary>
- public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
+ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
{
/// <summary>
/// Gets the name.
@@ -26,10 +26,10 @@ namespace MediaBrowser.Api.System
public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
{
_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);
}
@@ -38,15 +38,15 @@ namespace MediaBrowser.Api.System
/// Gets the data to send.
/// </summary>
/// <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)
{
- _activityManager.EntryCreated -= _activityManager_EntryCreated;
+ _activityManager.EntryCreated -= OnEntryCreated;
base.Dispose(dispose);
}
diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs
index 3d08d5437..bef91d54d 100644
--- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs
@@ -51,7 +51,7 @@ namespace MediaBrowser.Api.UserLibrary
public class ArtistsService : BaseItemsByNameService<MusicArtist>
{
public ArtistsService(
- ILogger<GenresService> logger,
+ ILogger<ArtistsService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
index c4a52d5f5..559082ff4 100644
--- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
+++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
@@ -28,7 +28,7 @@ namespace MediaBrowser.Api.UserLibrary
/// <param name="userDataRepository">The user data repository.</param>
/// <param name="dtoService">The dto service.</param>
protected BaseItemsByNameService(
- ILogger logger,
+ ILogger<BaseItemsByNameService<TItemType>> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
index c4d44042b..f3c0441e1 100644
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs
@@ -59,7 +59,7 @@ namespace MediaBrowser.Api.UserLibrary
/// <param name="localization">The localization.</param>
/// <param name="dtoService">The dto service.</param>
public ItemsService(
- ILogger logger,
+ ILogger<ItemsService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IUserManager userManager,
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index 77d567631..ef3f43c75 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -1,6 +1,4 @@
using System;
-using System.IO;
-using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Events;
@@ -12,11 +10,6 @@ namespace MediaBrowser.Controller.Devices
public interface IDeviceManager
{
/// <summary>
- /// Occurs when [camera image uploaded].
- /// </summary>
- event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
-
- /// <summary>
/// Saves the capabilities.
/// </summary>
/// <param name="reportedId">The reported identifier.</param>
@@ -46,22 +39,6 @@ namespace MediaBrowser.Controller.Devices
QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
/// <summary>
- /// Gets the upload history.
- /// </summary>
- /// <param name="deviceId">The device identifier.</param>
- /// <returns>ContentUploadHistory.</returns>
- ContentUploadHistory GetCameraUploadHistory(string deviceId);
-
- /// <summary>
- /// Accepts the upload.
- /// </summary>
- /// <param name="deviceId">The device identifier.</param>
- /// <param name="stream">The stream.</param>
- /// <param name="file">The file.</param>
- /// <returns>Task.</returns>
- Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file);
-
- /// <summary>
/// Determines whether this instance [can access device] the specified user identifier.
/// </summary>
bool CanAccessDevice(User user, string deviceId);
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 04ba0fabc..d1d6c74b8 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -39,10 +39,9 @@ namespace MediaBrowser.Controller
int HttpsPort { get; }
/// <summary>
- /// Gets a value indicating whether [supports HTTPS].
+ /// Gets a value indicating whether the server should listen on an HTTPS port.
/// </summary>
- /// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value>
- bool EnableHttps { get; }
+ bool ListenWithHttps { get; }
/// <summary>
/// Gets a value indicating whether this instance has update available.
@@ -57,34 +56,50 @@ namespace MediaBrowser.Controller
string FriendlyName { get; }
/// <summary>
- /// Gets the local ip address.
+ /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
+ /// to the API that should exist at the address.
/// </summary>
- /// <value>The local ip address.</value>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
+ /// <returns>A list containing all the local IP addresses of the server.</returns>
Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
/// <summary>
- /// Gets the local API URL.
+ /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
+ /// IP address that can be found via <see cref="GetLocalIpAddresses"/>. HTTPS will be preferred when available.
/// </summary>
- /// <param name="cancellationToken">Token to cancel the request if needed.</param>
- /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
- /// <value>The local API URL.</value>
- Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false);
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
+ /// <returns>The server URL.</returns>
+ Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
/// <summary>
- /// Gets the local API URL.
+ /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
+ /// over HTTP (not HTTPS).
/// </summary>
- /// <param name="hostname">The hostname.</param>
- /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
- /// <returns>The local API URL.</returns>
- string GetLocalApiUrl(ReadOnlySpan<char> hostname, bool forceHttp = false);
+ /// <returns>The API URL.</returns>
+ string GetLoopbackHttpApiUrl();
/// <summary>
- /// Gets the local API URL.
+ /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available.
/// </summary>
- /// <param name="address">The IP address.</param>
- /// <param name="forceHttp">Whether to force usage of plain HTTP protocol.</param>
- /// <returns>The local API URL.</returns>
- string GetLocalApiUrl(IPAddress address, bool forceHttp = false);
+ /// <param name="address">The IP address to use as the hostname in the URL.</param>
+ /// <returns>The API URL.</returns>
+ string GetLocalApiUrl(IPAddress address);
+
+ /// <summary>
+ /// Gets a local (LAN) URL that can be used to access the API.
+ /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair.
+ /// </summary>
+ /// <param name="hostname">The hostname to use in the URL.</param>
+ /// <param name="scheme">
+ /// The scheme to use for the URL. If null, the scheme will be selected automatically,
+ /// preferring HTTPS, if available.
+ /// </param>
+ /// <param name="port">
+ /// The port to use for the URL. If null, the port will be selected automatically,
+ /// preferring the HTTPS port, if available.
+ /// </param>
+ /// <returns>The API URL.</returns>
+ string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null);
/// <summary>
/// Open a URL in an external browser window.
@@ -101,7 +116,5 @@ namespace MediaBrowser.Controller
string ReverseVirtualPath(string path);
Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
-
- Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next);
}
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index b710318ee..1162bff13 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -77,8 +77,6 @@ namespace MediaBrowser.Controller.Net
return Task.CompletedTask;
}
- protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
/// <summary>
/// Starts sending messages over a web socket
/// </summary>
@@ -87,12 +85,12 @@ namespace MediaBrowser.Controller.Net
{
var vals = message.Data.Split(',');
- var dueTimeMs = long.Parse(vals[0], UsCulture);
- var periodMs = long.Parse(vals[1], UsCulture);
+ var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture);
+ var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture);
var cancellationTokenSource = new CancellationTokenSource();
- Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name);
+ Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name);
var state = new TStateType
{
@@ -154,7 +152,6 @@ namespace MediaBrowser.Controller.Net
{
MessageType = Name,
Data = data
-
}, cancellationToken).ConfigureAwait(false);
state.DateLastSendUtc = DateTime.UtcNow;
@@ -197,7 +194,7 @@ namespace MediaBrowser.Controller.Net
/// <param name="connection">The connection.</param>
private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
{
- Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
+ Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
// TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
// connection.Item1.Dispose();
@@ -242,6 +239,7 @@ namespace MediaBrowser.Controller.Net
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
}
diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs
index 806478864..efb5f4ac3 100644
--- a/MediaBrowser.Controller/Net/IHttpServer.cs
+++ b/MediaBrowser.Controller/Net/IHttpServer.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Services;
@@ -9,9 +8,9 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
/// <summary>
- /// Interface IHttpServer
+ /// Interface IHttpServer.
/// </summary>
- public interface IHttpServer : IDisposable
+ public interface IHttpServer
{
/// <summary>
/// Gets the URL prefix.
@@ -20,11 +19,6 @@ namespace MediaBrowser.Controller.Net
string[] UrlPrefixes { get; }
/// <summary>
- /// Stops this instance.
- /// </summary>
- void Stop();
-
- /// <summary>
/// Occurs when [web socket connected].
/// </summary>
event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
@@ -40,22 +34,17 @@ namespace MediaBrowser.Controller.Net
string GlobalResponse { get; set; }
/// <summary>
- /// Sends the http context to the socket listener
+ /// The HTTP request handler
/// </summary>
- /// <param name="ctx"></param>
+ /// <param name="context"></param>
/// <returns></returns>
- Task ProcessWebSocketRequest(HttpContext ctx);
+ Task RequestHandler(HttpContext context);
/// <summary>
- /// The HTTP request handler
+ /// Get the default CORS headers
/// </summary>
- /// <param name="httpReq"></param>
- /// <param name="urlString"></param>
- /// <param name="host"></param>
- /// <param name="localPath"></param>
- /// <param name="cancellationToken"></param>
+ /// <param name="req"></param>
/// <returns></returns>
- Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath,
- CancellationToken cancellationToken);
+ IDictionary<string, string> GetDefaultCorsHeaders(IRequest req);
}
}
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index 31eb7ccb7..09e43c683 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -1,4 +1,7 @@
+#nullable enable
+
using System;
+using System.Net;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
@@ -7,18 +10,12 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
- public interface IWebSocketConnection : IDisposable
+ public interface IWebSocketConnection
{
/// <summary>
/// Occurs when [closed].
/// </summary>
- event EventHandler<EventArgs> Closed;
-
- /// <summary>
- /// Gets the id.
- /// </summary>
- /// <value>The id.</value>
- Guid Id { get; }
+ event EventHandler<EventArgs>? Closed;
/// <summary>
/// Gets the last activity date.
@@ -27,21 +24,16 @@ namespace MediaBrowser.Controller.Net
DateTime LastActivityDate { get; }
/// <summary>
- /// Gets or sets the URL.
- /// </summary>
- /// <value>The URL.</value>
- string Url { get; set; }
- /// <summary>
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
- IQueryCollection QueryString { get; set; }
+ IQueryCollection QueryString { get; }
/// <summary>
/// Gets or sets the receive action.
/// </summary>
/// <value>The receive action.</value>
- Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+ Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
/// <summary>
/// Gets the state.
@@ -53,7 +45,7 @@ namespace MediaBrowser.Controller.Net
/// Gets the remote end point.
/// </summary>
/// <value>The remote end point.</value>
- string RemoteEndPoint { get; }
+ IPAddress? RemoteEndPoint { get; }
/// <summary>
/// Sends a message asynchronously.
@@ -65,21 +57,6 @@ namespace MediaBrowser.Controller.Net
/// <exception cref="ArgumentNullException">message</exception>
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <param name="buffer">The buffer.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendAsync(byte[] buffer, CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <param name="text">The text.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">buffer</exception>
- Task SendAsync(string text, CancellationToken cancellationToken);
+ Task ProcessAsync(CancellationToken cancellationToken = default);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index a59c96ac7..04450085b 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading;
using System.Threading.Tasks;
@@ -20,6 +21,6 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message.
/// </summary>
- Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken);
+ Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index f1f10a3a3..2ba7c9fec 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Session
{
/// <summary>
- /// Class SessionInfo
+ /// Class SessionInfo.
/// </summary>
- public class SessionInfo : IDisposable
+ public sealed class SessionInfo : IDisposable
{
- private ISessionManager _sessionManager;
+ // 1 second
+ private const long ProgressIncrement = 10000000;
+
+ private readonly ISessionManager _sessionManager;
private readonly ILogger _logger;
+
+ private readonly object _progressLock = new object();
+ private Timer _progressTimer;
+ private PlaybackProgressInfo _lastProgressInfo;
+
+ private bool _disposed = false;
+
public SessionInfo(ISessionManager sessionManager, ILogger logger)
{
_sessionManager = sessionManager;
@@ -97,8 +107,6 @@ namespace MediaBrowser.Controller.Session
/// <value>The name of the device.</value>
public string DeviceName { get; set; }
- public string DeviceType { get; set; }
-
/// <summary>
/// Gets or sets the now playing item.
/// </summary>
@@ -128,22 +136,6 @@ namespace MediaBrowser.Controller.Session
[JsonIgnore]
public ISessionController[] SessionControllers { get; set; }
- /// <summary>
- /// Gets or sets the supported commands.
- /// </summary>
- /// <value>The supported commands.</value>
- public string[] SupportedCommands
- {
- get
- {
- if (Capabilities == null)
- {
- return new string[] { };
- }
- return Capabilities.SupportedCommands;
- }
- }
-
public TranscodingInfo TranscodingInfo { get; set; }
/// <summary>
@@ -215,6 +207,14 @@ namespace MediaBrowser.Controller.Session
}
}
+ public QueueItem[] NowPlayingQueue { get; set; }
+
+ public bool HasCustomDeviceName { get; set; }
+
+ public string PlaylistItemId { get; set; }
+
+ public string UserPrimaryImageTag { get; set; }
+
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
{
var controllers = SessionControllers.ToList();
@@ -258,10 +258,6 @@ namespace MediaBrowser.Controller.Session
return false;
}
- private readonly object _progressLock = new object();
- private Timer _progressTimer;
- private PlaybackProgressInfo _lastProgressInfo;
-
public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
{
if (_disposed)
@@ -284,9 +280,6 @@ namespace MediaBrowser.Controller.Session
}
}
- // 1 second
- private const long ProgressIncrement = 10000000;
-
private async void OnProgressTimerCallback(object state)
{
if (_disposed)
@@ -345,8 +338,7 @@ namespace MediaBrowser.Controller.Session
}
}
- private bool _disposed = false;
-
+ /// <inheritdoc />
public void Dispose()
{
_disposed = true;
@@ -358,30 +350,12 @@ namespace MediaBrowser.Controller.Session
foreach (var controller in controllers)
{
- var disposable = controller as IDisposable;
-
- if (disposable != null)
+ if (controller is IDisposable disposable)
{
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
-
- try
- {
- disposable.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error disposing session controller");
- }
+ disposable.Dispose();
}
}
-
- _sessionManager = null;
}
-
- public QueueItem[] NowPlayingQueue { get; set; }
- public bool HasCustomDeviceName { get; set; }
- public string PlaylistItemId { get; set; }
- public string ServerId { get; set; }
- public string UserPrimaryImageTag { get; set; }
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index 2e328ba63..de35acbba 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -7,14 +7,25 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
+ /// <summary>
+ /// Subtitle writer for the WebVTT format.
+ /// </summary>
public class VttWriter : ISubtitleWriter
{
+ /// <inheritdoc />
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.WriteLine("WEBVTT");
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)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -22,13 +33,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks);
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)
{
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;
diff --git a/MediaBrowser.Model/Activity/ActivityLogEntry.cs b/MediaBrowser.Model/Activity/ActivityLogEntry.cs
index 80f01b66e..5ab904394 100644
--- a/MediaBrowser.Model/Activity/ActivityLogEntry.cs
+++ b/MediaBrowser.Model/Activity/ActivityLogEntry.cs
@@ -59,6 +59,7 @@ namespace MediaBrowser.Model.Activity
/// Gets or sets the user primary image tag.
/// </summary>
/// <value>The user primary image tag.</value>
+ [Obsolete("UserPrimaryImageTag is not used.")]
public string UserPrimaryImageTag { get; set; }
/// <summary>
diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs
index f336f5272..9dab5e77b 100644
--- a/MediaBrowser.Model/Activity/IActivityManager.cs
+++ b/MediaBrowser.Model/Activity/IActivityManager.cs
@@ -1,6 +1,9 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
@@ -10,10 +13,15 @@ namespace MediaBrowser.Model.Activity
{
event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
- void Create(ActivityLogEntry entry);
+ void Create(ActivityLog entry);
- QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit);
+ Task CreateAsync(ActivityLog entry);
- QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? x, int? y);
+ QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit);
+
+ QueryResult<ActivityLogEntry> GetPagedResult(
+ Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
+ int? startIndex,
+ int? limit);
}
}
diff --git a/MediaBrowser.Model/Activity/IActivityRepository.cs b/MediaBrowser.Model/Activity/IActivityRepository.cs
deleted file mode 100644
index 66144ec47..000000000
--- a/MediaBrowser.Model/Activity/IActivityRepository.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.Querying;
-
-namespace MediaBrowser.Model.Activity
-{
- public interface IActivityRepository
- {
- void Create(ActivityLogEntry entry);
-
- QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? z, int? startIndex, int? limit);
- }
-}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 063ccd9b9..1f5981f10 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -49,17 +49,24 @@ namespace MediaBrowser.Model.Configuration
public int HttpsPortNumber { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether [use HTTPS].
+ /// Gets or sets a value indicating whether to use HTTPS.
/// </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 EnableNormalizedItemByNameIds { get; set; }
/// <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>
- /// <value>The value pointing to the file system where the ssl certificate is located..</value>
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; }
/// <summary>
@@ -69,8 +76,9 @@ namespace MediaBrowser.Model.Configuration
public bool IsPortAuthorized { get; set; }
public bool AutoRunWebApp { get; set; }
+
public bool EnableRemoteAccess { get; set; }
- public bool CameraUploadUpgraded { get; set; }
+
public bool CollectionsUpgraded { get; set; }
/// <summary>
@@ -86,6 +94,7 @@ namespace MediaBrowser.Model.Configuration
/// </summary>
/// <value>The metadata path.</value>
public string MetadataPath { get; set; }
+
public string MetadataNetworkPath { get; set; }
/// <summary>
@@ -208,15 +217,26 @@ namespace MediaBrowser.Model.Configuration
public int RemoteClientBitrateLimit { get; set; }
public bool EnableFolderView { get; set; }
+
public bool EnableGroupingIntoCollections { get; set; }
+
public bool DisplaySpecialsWithinSeasons { get; set; }
+
public string[] LocalNetworkSubnets { get; set; }
+
public string[] LocalNetworkAddresses { get; set; }
+
public string[] CodecsUsed { get; set; }
+
public bool IgnoreVirtualInterfaces { get; set; }
+
public bool EnableExternalContentInSuggestions { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the server should force connections over HTTPS.
+ /// </summary>
public bool RequireHttps { get; set; }
- public bool IsBehindProxy { get; set; }
+
public bool EnableNewOmdbSupport { get; set; }
public string[] RemoteIPFilter { get; set; }
diff --git a/MediaBrowser.Model/Devices/DeviceOptions.cs b/MediaBrowser.Model/Devices/DeviceOptions.cs
new file mode 100644
index 000000000..8b77fd7fc
--- /dev/null
+++ b/MediaBrowser.Model/Devices/DeviceOptions.cs
@@ -0,0 +1,9 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Devices
+{
+ public class DeviceOptions
+ {
+ public string CustomName { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Devices/DevicesOptions.cs b/MediaBrowser.Model/Devices/DevicesOptions.cs
deleted file mode 100644
index 02570650e..000000000
--- a/MediaBrowser.Model/Devices/DevicesOptions.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Devices
-{
- public class DevicesOptions
- {
- public string[] EnabledCameraUploadDevices { get; set; }
- public string CameraUploadPath { get; set; }
- public bool EnableCameraUploadSubfolders { get; set; }
-
- public DevicesOptions()
- {
- EnabledCameraUploadDevices = Array.Empty<string>();
- }
- }
-
- public class DeviceOptions
- {
- public string CustomName { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index b41d0af1d..5c6e313e0 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -37,6 +37,9 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
+ </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index fe2fbe7e4..b491a015c 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -67,6 +67,7 @@ namespace MediaBrowser.Model.Net
{ ".m3u8", "application/x-mpegURL" },
{ ".map", "application/x-javascript" },
{ ".mobi", "application/x-mobipocket-ebook" },
+ { ".opf", "application/oebps-package+xml" },
{ ".pdf", "application/pdf" },
{ ".rar", "application/vnd.rar" },
{ ".srt", "application/x-subrip" },
diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs
index 7575224d4..03f03e4cc 100644
--- a/MediaBrowser.Model/Net/WebSocketMessage.cs
+++ b/MediaBrowser.Model/Net/WebSocketMessage.cs
@@ -1,5 +1,8 @@
+
#pragma warning disable CS1591
+using System;
+
namespace MediaBrowser.Model.Net
{
/// <summary>
@@ -13,7 +16,9 @@ namespace MediaBrowser.Model.Net
/// </summary>
/// <value>The type of the message.</value>
public string MessageType { get; set; }
- public string MessageId { get; set; }
+
+ public Guid MessageId { get; set; }
+
public string ServerId { get; set; }
/// <summary>
@@ -22,5 +27,4 @@ namespace MediaBrowser.Model.Net
/// <value>The data.</value>
public T Data { get; set; }
}
-
}
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index 3d543039e..f2c5aa1e3 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -116,24 +116,6 @@ namespace MediaBrowser.Model.System
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>
/// Gets or sets a value indicating whether this instance has update available.
/// </summary>
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
index 37160dd2c..f0328e8d8 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
@@ -11,13 +11,10 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Omdb
{
- public class OmdbEpisodeProvider :
- IRemoteMetadataProvider<Episode, EpisodeInfo>,
- IHasOrder
+ public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient;
@@ -26,16 +23,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
private readonly IServerConfigurationManager _configurationManager;
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;
_httpClient = httpClient;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_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)
{
return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken);
@@ -66,10 +74,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return result;
}
- // After TheTvDb
- public int Order => 1;
-
- public string Name => "The Open Movie Database";
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 3aadda5d0..64a75955a 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -17,7 +17,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Omdb
{
@@ -26,22 +25,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient;
- private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
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;
_httpClient = httpClient;
- _logger = logger;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
_appHost = appHost;
}
+
// After primary option
public int Order => 2;
@@ -80,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var parsedName = _libraryManager.ParseName(name);
var yearInName = parsedName.Year;
name = parsedName.Name;
- year = year ?? yearInName;
+ year ??= yearInName;
}
if (string.IsNullOrWhiteSpace(imdbId))
@@ -312,6 +316,5 @@ namespace MediaBrowser.Providers.Plugins.Omdb
/// <value>The results.</value>
public List<SearchResult> Search { get; set; }
}
-
}
}
diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs
index 7195dc42a..6e3c26c26 100644
--- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs
@@ -27,9 +27,6 @@ namespace MediaBrowser.Providers.Tmdb.TV
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 readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- internal static TmdbSeriesProvider Current { get; private set; }
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
@@ -39,6 +36,10 @@ namespace MediaBrowser.Providers.Tmdb.TV
private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager;
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ internal static TmdbSeriesProvider Current { get; private set; }
+
public TmdbSeriesProvider(
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
@@ -217,10 +218,9 @@ namespace MediaBrowser.Providers.Tmdb.TV
var series = seriesResult.Item;
series.Name = seriesInfo.Name;
+ series.OriginalTitle = seriesInfo.Original_Name;
series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture));
- //series.VoteCount = seriesInfo.vote_count;
-
string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
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.HomePageUrl = seriesInfo.homepage;
+ series.HomePageUrl = seriesInfo.Homepage;
series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
@@ -308,29 +308,61 @@ namespace MediaBrowser.Providers.Tmdb.TV
seriesResult.ResetPeople();
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(),
- Role = actor.Character,
- Type = PersonType.Actor,
- SortOrder = actor.Order
- };
+ var personInfo = new PersonInfo
+ {
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ };
- if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
- {
- personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
+ 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 (actor.Id > 0)
+ if (seriesInfo.Credits.Crew != null)
+ {
+ var keepTypes = new[]
{
- personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
- }
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
- seriesResult.AddPerson(personInfo);
+ foreach (var person in seriesInfo.Credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ seriesResult.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
}
}
}
diff --git a/MediaBrowser.Providers/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Tmdb/TmdbUtils.cs
index 035b99c1a..7dacc7404 100644
--- a/MediaBrowser.Providers/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Tmdb/TmdbUtils.cs
@@ -4,18 +4,51 @@ using MediaBrowser.Providers.Tmdb.Models.General;
namespace MediaBrowser.Providers.Tmdb
{
+ /// <summary>
+ /// Utilities for the TMDb provider
+ /// </summary>
public static class TmdbUtils
{
+ /// <summary>
+ /// URL of the TMDB instance to use.
+ /// </summary>
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/";
+
+ /// <summary>
+ /// Name of the provider.
+ /// </summary>
public const string ProviderName = "TheMovieDb";
+
+ /// <summary>
+ /// API key to use when performing an API call.
+ /// </summary>
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
+
+ /// <summary>
+ /// Value of the Accept header for requests to the provider.
+ /// </summary>
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)
{
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;
}
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 6b9fed83b..e100c0b1c 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -66,9 +66,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}"
EndProject
Global
@@ -185,10 +186,10 @@ Global
{F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = Release|Any CPU
- {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU
{7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU